diff --git a/back/api/config.ts b/back/api/config.ts index f40a8cab..23084cfe 100644 --- a/back/api/config.ts +++ b/back/api/config.ts @@ -4,7 +4,7 @@ import { Logger } from 'winston'; import config from '../config'; import * as fs from 'fs/promises'; import { celebrate, Joi } from 'celebrate'; -import { join } from 'path'; +import { join, basename } from 'path'; import { SAMPLE_FILES } from '../config/const'; import { t } from '../shared/i18n'; import ConfigService from '../services/config'; @@ -72,20 +72,23 @@ export default (app: Router) => { const logger: Logger = Container.get('logger'); try { const { name, content } = req.body; - if (config.blackFileList.includes(name)) { - res.send({ code: 403, message: t('文件无法访问') }); - } - let path = join(config.configPath, name); + // Resolve path first to prevent traversal attacks + let basePath = config.configPath; if (name.startsWith('data/scripts/')) { - path = join(config.rootPath, name); + basePath = join(config.rootPath, 'data/scripts'); } - if ( - !path.startsWith(config.configPath) && - !path.startsWith(config.scriptPath) - ) { + const cleanName = name.replace(/^data\/scripts\//, ''); + const resolvedPath = join(basePath, cleanName); + const normalized = join(resolvedPath); + // Verify the resolved path stays within allowed directory + if (!normalized.startsWith(basePath)) { return res.send({ code: 403, message: t('文件路径无效') }); } - await writeFileWithLock(path, content); + // Check blacklist on actual filename (not user input) + if (config.blackFileList.includes(basename(normalized))) { + return res.send({ code: 403, message: t('文件无法访问') }); + } + await writeFileWithLock(normalized, content); res.send({ code: 200, message: t('保存成功') }); } catch (e) { return next(e); diff --git a/back/services/config.ts b/back/services/config.ts index 7e7e5985..0c5ea30a 100644 --- a/back/services/config.ts +++ b/back/services/config.ts @@ -12,18 +12,24 @@ export default class ConfigService { public async getFile(filePath: string, res: Response) { let content = ''; - const avaliablePath = [config.rootPath, config.configPath].map((x) => - path.resolve(x, filePath), - ); - - if ( - config.blackFileList.includes(filePath) || - avaliablePath.every( - (x) => - !x.startsWith(config.scriptPath) && !x.startsWith(config.configPath), - ) || - !filePath - ) { + if (!filePath) { + return res.send({ code: 403, message: t('文件无法访问') }); + } + const normalized = path.normalize(filePath); + if (normalized.startsWith('..') || path.isAbsolute(normalized)) { + return res.send({ code: 403, message: t('文件无法访问') }); + } + const resolvedRoot = path.resolve(config.rootPath, normalized); + const resolvedConfig = path.resolve(config.configPath, normalized); + const isValidPath = + resolvedRoot.startsWith(config.scriptPath) || + resolvedRoot.startsWith(config.configPath) || + resolvedConfig.startsWith(config.scriptPath) || + resolvedConfig.startsWith(config.configPath); + if (!isValidPath) { + return res.send({ code: 403, message: t('文件无法访问') }); + } + if (config.blackFileList.includes(path.basename(normalized))) { return res.send({ code: 403, message: t('文件无法访问') }); }