Skip to content

Commit 285632f

Browse files
Copilotsj817
andauthored
fix: preserve user napcat.json config across updates (#1765)
* Initial plan * fix: prevent o3HookMode from being reset to enabled after NapCat updates Agent-Logs-Url: https://github.com/NapNeko/NapCatQQ/sessions/98ea8e7d-4ee4-43c1-ac33-2714fba480e3 Co-authored-by: sj817 <74231782+sj817@users.noreply.github.com> * fix: restore o3HookMode default to 1, harden updater against path traversal Agent-Logs-Url: https://github.com/NapNeko/NapCatQQ/sessions/53b58325-1685-4961-803e-93091dd8b1c4 Co-authored-by: sj817 <74231782+sj817@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sj817 <74231782+sj817@users.noreply.github.com>
1 parent 6d63da4 commit 285632f

1 file changed

Lines changed: 27 additions & 3 deletions

File tree

packages/napcat-webui-backend/src/api/UpdateNapCat.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ const SKIP_UPDATE_FILES = [
4242
'NapCatWinBootHook.dll',
4343
];
4444

45+
// 更新时若文件已存在则保留(不覆盖)的用户配置文件(使用相对路径精确匹配)
46+
// 这些文件保存了用户的自定义设置,更新时应予以保留;
47+
// 新增的配置项将在运行时通过 TypeBox schema 默认值自动填充,用户不会错过新配置选项。
48+
const PRESERVE_USER_CONFIG_RELATIVE_PATHS = new Set([
49+
path.normalize('config/napcat.json'),
50+
]);
51+
4552
/**
4653
* 递归扫描目录中的所有文件
4754
*/
@@ -59,9 +66,12 @@ function scanFilesRecursively (dirPath: string, basePath: string = dirPath): Arr
5966
for (const item of items) {
6067
const fullPath = path.join(dirPath, item);
6168
const relativePath = path.relative(basePath, fullPath);
62-
const stat = fs.statSync(fullPath);
69+
const stat = fs.lstatSync(fullPath);
6370

64-
if (stat.isDirectory()) {
71+
if (stat.isSymbolicLink()) {
72+
// 跳过符号链接,避免潜在的路径穿越风险
73+
continue;
74+
} else if (stat.isDirectory()) {
6575
// 递归扫描子目录
6676
files.push(...scanFilesRecursively(fullPath, basePath));
6777
} else if (stat.isFile()) {
@@ -263,15 +273,29 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => {
263273
}> = [];
264274

265275
// 先尝试直接替换文件
276+
const resolvedBinaryPath = path.resolve(webUiPathWrapper.binaryPath);
266277
for (const fileInfo of allFiles) {
267-
const targetFilePath = path.join(webUiPathWrapper.binaryPath, fileInfo.relativePath);
278+
// 防止路径穿越攻击:确保目标路径严格位于 binaryPath 子目录内
279+
const targetFilePath = path.resolve(webUiPathWrapper.binaryPath, fileInfo.relativePath);
280+
if (!targetFilePath.startsWith(resolvedBinaryPath + path.sep)) {
281+
webUiLogger?.logWarn(`[NapCat Update] Skipping suspicious path: ${fileInfo.relativePath}`);
282+
continue;
283+
}
284+
285+
const normalizedRelativePath = path.normalize(fileInfo.relativePath);
268286

269287
// 跳过指定的文件
270288
if (SKIP_UPDATE_FILES.includes(path.basename(fileInfo.relativePath))) {
271289
webUiLogger?.log(`[NapCat Update] Skipping update for ${fileInfo.relativePath}`);
272290
continue;
273291
}
274292

293+
// 保留已存在的用户配置文件,避免覆盖用户设置
294+
if (PRESERVE_USER_CONFIG_RELATIVE_PATHS.has(normalizedRelativePath) && fs.existsSync(targetFilePath)) {
295+
webUiLogger?.log(`[NapCat Update] Preserving existing user config: ${fileInfo.relativePath}`);
296+
continue;
297+
}
298+
275299
try {
276300
// 确保目标目录存在
277301
const targetDir = path.dirname(targetFilePath);

0 commit comments

Comments
 (0)