fix: 修复代码审查发现的5个安全与正确性问题

- pbkdf2Sync 改为异步避免阻塞事件循环 (auth.js, config-api.js)
- 路径遍历检查改用 path.resolve 验证绝对路径在 cwd 内 (config-api.js)
- _activeInterval 移出配置对象避免序列化到 JSON (config-api.js, api-server.js)
- 删除 performScheduledHealthChecks 中冗余的 isDisabled 二次检查 (provider-pool-manager.js)
This commit is contained in:
Wenaixi 2026-04-02 23:27:10 +08:00
parent fca9413f26
commit 9d4864dfed
4 changed files with 22 additions and 16 deletions

View file

@ -1834,11 +1834,6 @@ export class ProviderPoolManager {
let failCount = 0;
for (const { providerType, provider, uuid, customName } of providersToCheck) {
// Skip if provider became disabled during iteration
if (provider.config.isDisabled === true) {
continue;
}
const providerCheckStart = Date.now();
const checkModelName = provider.config.checkModelName || ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType] || 'unknown';
const displayName = customName || uuid.substring(0, 8);

View file

@ -420,9 +420,10 @@ async function startServer() {
// 设置定时任务
runHealthCheckTimer(interval);
// 导出重载函数供外部调用
// 注册重载函数和初始 interval 到 globalThis供 config-api 热更新使用)
globalThis.reloadHealthCheckTimer = runHealthCheckTimer;
globalThis._activeHealthCheckInterval = interval;
}
// 如果是子进程,通知主进程已就绪

View file

@ -55,7 +55,11 @@ export async function validateCredentials(password) {
const parts = storedPassword.split(':');
if (parts.length !== 3) return false;
const [, salt, storedHash] = parts;
const inputHash = crypto.pbkdf2Sync(password.trim(), salt, 100000, 64, 'sha512').toString('hex');
const inputHash = await new Promise((resolve, reject) =>
crypto.pbkdf2(password.trim(), salt, 100000, 64, 'sha512', (err, key) =>
err ? reject(err) : resolve(key.toString('hex'))
)
);
return crypto.timingSafeEqual(Buffer.from(inputHash, 'hex'), Buffer.from(storedHash, 'hex'));
}

View file

@ -125,8 +125,11 @@ export async function handleUpdateConfig(req, res, currentConfig) {
if (newConfig.MODEL_PROVIDER !== undefined) currentConfig.MODEL_PROVIDER = newConfig.MODEL_PROVIDER;
if (newConfig.SYSTEM_PROMPT_FILE_PATH !== undefined) {
const p = String(newConfig.SYSTEM_PROMPT_FILE_PATH);
// 防止路径遍历:只允许相对路径或限定目录
if (!p.includes('..')) currentConfig.SYSTEM_PROMPT_FILE_PATH = p;
// 防止路径遍历:解析后的绝对路径必须在工作目录内
const resolved = path.resolve(process.cwd(), p);
if (resolved.startsWith(process.cwd() + path.sep) || resolved === process.cwd()) {
currentConfig.SYSTEM_PROMPT_FILE_PATH = p;
}
}
if (newConfig.SYSTEM_PROMPT_MODE !== undefined) currentConfig.SYSTEM_PROMPT_MODE = newConfig.SYSTEM_PROMPT_MODE;
if (newConfig.PROMPT_LOG_BASE_NAME !== undefined) currentConfig.PROMPT_LOG_BASE_NAME = newConfig.PROMPT_LOG_BASE_NAME;
@ -180,11 +183,10 @@ export async function handleUpdateConfig(req, res, currentConfig) {
interval: newInterval,
providerTypes: Array.isArray(incoming?.providerTypes) ? incoming.providerTypes : []
};
// 如果定时器已存在且 enabled仅在 interval 实际变化时重新加载 timer
const previousInterval = currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval;
if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled && newInterval !== previousInterval) {
currentConfig.SCHEDULED_HEALTH_CHECK._activeInterval = newInterval;
// 仅在 interval 实际变化时重新加载 timer_activeInterval 存在内存变量中,不写入配置文件)
if (globalThis.reloadHealthCheckTimer && currentConfig.SCHEDULED_HEALTH_CHECK.enabled && newInterval !== globalThis._activeHealthCheckInterval) {
globalThis._activeHealthCheckInterval = newInterval;
globalThis.reloadHealthCheckTimer(newInterval);
}
}
@ -353,7 +355,11 @@ export async function handleUpdateAdminPassword(req, res) {
// 使用 PBKDF2 哈希存储密码,避免明文写入文件
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password.trim(), salt, 100000, 64, 'sha512').toString('hex');
const hash = await new Promise((resolve, reject) =>
crypto.pbkdf2(password.trim(), salt, 100000, 64, 'sha512', (err, key) =>
err ? reject(err) : resolve(key.toString('hex'))
)
);
const stored = `pbkdf2:${salt}:${hash}`;
const pwdFilePath = path.join(process.cwd(), 'configs', 'pwd');