From 1bb2a5ea19f95a2b1918dbf979e894ed3eb23451 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Mon, 27 Apr 2026 00:13:19 +0200 Subject: [PATCH] Fix missing filename sanitization on V2 JSON character import + harden getPngName as safety nee (#5538) * fix: sanitize character filenames on V2 JSON import and harden getPngName - Add missing sanitize() call in importFromJson V2 spec branch to match all other import paths - Sanitize data.name before readFromV2() so the name field sync happens automatically - Add sanitize() as defense-in-depth inside getPngName() to catch future oversights - Refactor getPngName() to use getUniqueName() utility for consistent name generation * fix: sanitize data.name before readFromV2 in importFromPng and importFromCharX Same bug as importFromJson: readFromV2() overwrites the top-level name with the unsanitized data.name, undoing any prior sanitize() call. Fix by sanitizing data.name before readFromV2 so the sync preserves it. * fix: sanitize top-level name field in JSON and CharX import paths * fix: incorrect path rejection in isPathUnderParent * fix: increase maxTries in getPngName --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> --- src/endpoints/characters.js | 24 +++++++++++++++--------- src/util.js | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 061547f3f..8e2a9dc55 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -772,10 +772,13 @@ async function importFromCharX(uploadPath, { request }, preservedFileName) { const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse(); // Apply standard character transformations + if (card.data?.name) { + card.data.name = sanitize(card.data.name); + } + card.name = sanitize(card.data?.name || card.name); let processedCard = readFromV2(card); unsetPrivateFields(processedCard); processedCard.create_date = new Date().toISOString(); - processedCard.name = sanitize(processedCard.name); const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories); // Use the actual character name for asset folders, not the unique filename @@ -887,9 +890,13 @@ async function importFromJson(uploadPath, { request }, preservedFileName) { console.info(`Importing from ${jsonData.spec} json`); importRisuSprites(request.user.directories, jsonData); unsetPrivateFields(jsonData); + if (jsonData.data?.name) { + jsonData.data.name = sanitize(jsonData.data.name); + } + jsonData.name = sanitize(jsonData.data?.name || jsonData.name); jsonData = readFromV2(jsonData); jsonData.create_date = new Date().toISOString(); - const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories); + const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories); const char = JSON.stringify(jsonData); const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request); return result ? pngName : ''; @@ -964,6 +971,9 @@ async function importFromPng(uploadPath, { request }, preservedFileName) { let jsonData = JSON.parse(imgData); + if (jsonData.data?.name) { + jsonData.data.name = sanitize(jsonData.data.name); + } jsonData.name = sanitize(jsonData.data?.name || jsonData.name); const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories); @@ -1529,13 +1539,9 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp * @returns {string} - The name for the uploaded PNG file */ function getPngName(file, directories) { - let i = 1; - const baseName = file; - while (fs.existsSync(path.join(directories.characters, `${file}.png`))) { - file = baseName + i; - i++; - } - return file; + file = sanitize(file); + return getUniqueName(file, (name) => fs.existsSync(path.join(directories.characters, `${name}.png`)), + { nameBuilder: (base, i) => i === 0 ? base : `${base}${i}`, startIndex: 0, maxTries: 10000 }) ?? file; } /** diff --git a/src/util.js b/src/util.js index ea3e50b21..98c1e9d21 100644 --- a/src/util.js +++ b/src/util.js @@ -1387,7 +1387,7 @@ export function isPathUnderParent(parentPath, childPath) { const relativePath = path.relative(normalizedParent, normalizedChild); - return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); + return relativePath !== '..' && !relativePath.startsWith('..' + path.sep) && !path.isAbsolute(relativePath); } /**