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>
This commit is contained in:
Wolfsblvt
2026-04-27 00:13:19 +02:00
committed by GitHub
parent 7201d87f2e
commit 1bb2a5ea19
2 changed files with 16 additions and 10 deletions
+15 -9
View File
@@ -772,10 +772,13 @@ async function importFromCharX(uploadPath, { request }, preservedFileName) {
const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse(); const { card, avatar, auxiliaryAssets, extractedBuffers } = await parser.parse();
// Apply standard character transformations // 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); let processedCard = readFromV2(card);
unsetPrivateFields(processedCard); unsetPrivateFields(processedCard);
processedCard.create_date = new Date().toISOString(); processedCard.create_date = new Date().toISOString();
processedCard.name = sanitize(processedCard.name);
const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories); const fileName = preservedFileName || getPngName(processedCard.name, request.user.directories);
// Use the actual character name for asset folders, not the unique filename // 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`); console.info(`Importing from ${jsonData.spec} json`);
importRisuSprites(request.user.directories, jsonData); importRisuSprites(request.user.directories, jsonData);
unsetPrivateFields(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 = readFromV2(jsonData);
jsonData.create_date = new Date().toISOString(); 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 char = JSON.stringify(jsonData);
const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request); const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);
return result ? pngName : ''; return result ? pngName : '';
@@ -964,6 +971,9 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
let jsonData = JSON.parse(imgData); let jsonData = JSON.parse(imgData);
if (jsonData.data?.name) {
jsonData.data.name = sanitize(jsonData.data.name);
}
jsonData.name = sanitize(jsonData.data?.name || jsonData.name); jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories); 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 * @returns {string} - The name for the uploaded PNG file
*/ */
function getPngName(file, directories) { function getPngName(file, directories) {
let i = 1; file = sanitize(file);
const baseName = file; return getUniqueName(file, (name) => fs.existsSync(path.join(directories.characters, `${name}.png`)),
while (fs.existsSync(path.join(directories.characters, `${file}.png`))) { { nameBuilder: (base, i) => i === 0 ? base : `${base}${i}`, startIndex: 0, maxTries: 10000 }) ?? file;
file = baseName + i;
i++;
}
return file;
} }
/** /**
+1 -1
View File
@@ -1387,7 +1387,7 @@ export function isPathUnderParent(parentPath, childPath) {
const relativePath = path.relative(normalizedParent, normalizedChild); const relativePath = path.relative(normalizedParent, normalizedChild);
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); return relativePath !== '..' && !relativePath.startsWith('..' + path.sep) && !path.isAbsolute(relativePath);
} }
/** /**