修复配置文件路径可能越权

This commit is contained in:
whyour 2026-06-12 23:45:40 +08:00
parent 6796068523
commit d1dfde3ca9
2 changed files with 32 additions and 23 deletions

View File

@ -4,7 +4,7 @@ import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { join } from 'path'; import { join, basename } from 'path';
import { SAMPLE_FILES } from '../config/const'; import { SAMPLE_FILES } from '../config/const';
import { t } from '../shared/i18n'; import { t } from '../shared/i18n';
import ConfigService from '../services/config'; import ConfigService from '../services/config';
@ -72,20 +72,23 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const { name, content } = req.body; const { name, content } = req.body;
if (config.blackFileList.includes(name)) { // Resolve path first to prevent traversal attacks
res.send({ code: 403, message: t('文件无法访问') }); let basePath = config.configPath;
}
let path = join(config.configPath, name);
if (name.startsWith('data/scripts/')) { if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name); basePath = join(config.rootPath, 'data/scripts');
} }
if ( const cleanName = name.replace(/^data\/scripts\//, '');
!path.startsWith(config.configPath) && const resolvedPath = join(basePath, cleanName);
!path.startsWith(config.scriptPath) const normalized = join(resolvedPath);
) { // Verify the resolved path stays within allowed directory
if (!normalized.startsWith(basePath)) {
return res.send({ code: 403, message: t('文件路径无效') }); 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('保存成功') }); res.send({ code: 200, message: t('保存成功') });
} catch (e) { } catch (e) {
return next(e); return next(e);

View File

@ -12,18 +12,24 @@ export default class ConfigService {
public async getFile(filePath: string, res: Response) { public async getFile(filePath: string, res: Response) {
let content = ''; let content = '';
const avaliablePath = [config.rootPath, config.configPath].map((x) => if (!filePath) {
path.resolve(x, filePath), return res.send({ code: 403, message: t('文件无法访问') });
); }
const normalized = path.normalize(filePath);
if ( if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
config.blackFileList.includes(filePath) || return res.send({ code: 403, message: t('文件无法访问') });
avaliablePath.every( }
(x) => const resolvedRoot = path.resolve(config.rootPath, normalized);
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath), const resolvedConfig = path.resolve(config.configPath, normalized);
) || const isValidPath =
!filePath 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('文件无法访问') }); return res.send({ code: 403, message: t('文件无法访问') });
} }