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) {
772 const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse();772 const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse();
773773
774 // Apply standard character transformations774 // 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);
775 let processedCard = readFromV2(card);779 let processedCard = readFromV2(card);
776 unsetPrivateFields(processedCard);780 unsetPrivateFields(processedCard);
777 processedCard.create_date = new Date().toISOString();781 processedCard.create_date = new Date().toISOString();
778 processedCard.name = sanitize(processedCard.name);
779782
780 const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories);783 const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories);
781 // Use the actual character name for asset folders, not the unique filename784 // Use the actual character name for asset folders, not the unique filename
@@ -887,9 +890,13 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
887 console.info(`Importing from ${jsonData.spec} json`);890 console.info(`Importing from ${jsonData.spec} json`);
888 importRisuSprites(request.user.directories, jsonData);891 importRisuSprites(request.user.directories, jsonData);
889 unsetPrivateFields(jsonData);892 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);
890 jsonData = readFromV2(jsonData);897 jsonData = readFromV2(jsonData);
891 jsonData.create_date = new Date().toISOString();898 jsonData.create_date = new Date().toISOString();
892 const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);899 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
893 const char = JSON.stringify(jsonData);900 const char = JSON.stringify(jsonData);
894 const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);901 const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);
895 return result ? pngName : '';902 return result ? pngName : '';
@@ -964,6 +971,9 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
964971
965 let jsonData = JSON.parse(imgData);972 let jsonData = JSON.parse(imgData);
966973
974 if (jsonData.data?.name) {
975 jsonData.data.name = sanitize(jsonData.data.name);
976 }
967 jsonData.name = sanitize(jsonData.data?.name || jsonData.name);977 jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
968 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);978 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
969979
@@ -1529,13 +1539,9 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp
1529 * @returns {string} - The name for the uploaded PNG file1539 * @returns {string} - The name for the uploaded PNG file
1530 */1540 */
1531function getPngName(file, directories) {1541function getPngName(file, directories) {
1532 let i = 1;1542 file = sanitize(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;
1539}1545}
15401546
1541/**1547/**
src/util.js+1 -1
@@ -1387,7 +1387,7 @@ export function isPathUnderParent(parentPath, childPath) {
13871387
1388 const relativePath = path.relative(normalizedParent, normalizedChild);1388 const relativePath = path.relative(normalizedParent, normalizedChild);
13891389
1390 return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);1390 return relativePath !== '..' && !relativePath.startsWith('..' + path.sep) && !path.isAbsolute(relativePath);
1391}1391}
13921392
1393/**1393/**