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>

1bb2a5ea19f95a2b1918dbf979e894ed3eb23451

Wolfsblvt <wolfsblvt@gmail.com>

Signed
2 files changed, +16 -10Ignore whitespace
src/endpoints/characters.js+15 -9
@@ -772,10 +772,13 @@ async function importFromCharX(uploadPath, { request }, preservedFileName) {
772772 const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse();
773773
774774 // Apply standard character transformations
775+ if (card.data?.name) {
776+ card.data.name = sanitize(card.data.name);
777+ }
778+ card.name = sanitize(card.data?.name || card.name);
775779 let processedCard = readFromV2(card);
776780 unsetPrivateFields(processedCard);
777781 processedCard.create_date = new Date().toISOString();
778- processedCard.name = sanitize(processedCard.name);
779782
780783 const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories);
781784 // Use the actual character name for asset folders, not the unique filename
@@ -887,9 +890,13 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
887890 console.info(`Importing from ${jsonData.spec} json`);
888891 importRisuSprites(request.user.directories, jsonData);
889892 unsetPrivateFields(jsonData);
893+ if (jsonData.data?.name) {
894+ jsonData.data.name = sanitize(jsonData.data.name);
895+ }
896+ jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
890897 jsonData = readFromV2(jsonData);
891898 jsonData.create_date = new Date().toISOString();
892899 const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
893900 const char = JSON.stringify(jsonData);
894901 const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);
895902 return result ? pngName : '';
@@ -964,6 +971,9 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
964971
965972 let jsonData = JSON.parse(imgData);
966973
974+ if (jsonData.data?.name) {
975+ jsonData.data.name = sanitize(jsonData.data.name);
976+ }
967977 jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
968978 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
969979
@@ -1529,13 +1539,9 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp
15291539 * @returns {string} - The name for the uploaded PNG file
15301540 */
15311541function getPngName(file, directories) {
15321542 let ifile = 1sanitize(file);
1533- const baseName = file;
1543+ return getUniqueName(file, (name) => fs.existsSync(path.join(directories.characters, `${name}.png`)),
1534- while (fs.existsSync(path.join(directories.characters, `${file}.png`))) {
1544+ { nameBuilder: (base, i) => i === 0 ? base : `${base}${i}`, startIndex: 0, maxTries: 10000 }) ?? file;
1535- file = baseName + i;
1536- i++;
1537- }
1538- return file;
15391545}
15401546
15411547/**
src/util.js+1 -1
@@ -1387,7 +1387,7 @@ export function isPathUnderParent(parentPath, childPath) {
13871387
13881388 const relativePath = path.relative(normalizedParent, normalizedChild);
13891389
13901390 return relativePath !== '..' && !relativePath.startsWith('..' + path.sep) && !path.isAbsolute(relativePath);
13911391}
13921392
13931393/**