Add path validation for chat directory operations
| @@ -20,6 +20,7 @@ import { | ||
| 20 | 20 | tryReadFileSync, |
| 21 | 21 | tryDeleteFile, |
| 22 | 22 | readFirstLine, |
| 23 | + isPathUnderParent, | |
| 23 | 24 | } from '../util.js'; |
| 24 | 25 | |
| 25 | 26 | const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean'); |
| @@ -473,6 +474,9 @@ router.post('/save', validateAvatarUrlMiddleware, async function (request, respo | ||
| 473 | 474 | const chatData = request.body.chat; |
| 474 | 475 | const chatFileName = `${String(request.body.file_name)}.jsonl`; |
| 475 | 476 | const chatFilePath = path.join(request.user.directories.chats, cardName, sanitize(chatFileName)); |
| 477 | + if (!isPathUnderParent(request.user.directories.chats, chatFilePath)) { | |
| 478 | + return response.sendStatus(400); | |
| 479 | + } | |
| 476 | 480 | |
| 477 | 481 | if (Array.isArray(chatData)) { |
| 478 | 482 | await trySaveChat(chatData, chatFilePath, request.body.force, handle, cardName, request.user.directories.backups); |
| @@ -514,6 +518,9 @@ router.post('/get', validateAvatarUrlMiddleware, function (request, response) { | ||
| 514 | 518 | try { |
| 515 | 519 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 516 | 520 | const directoryPath = path.join(request.user.directories.chats, dirName); |
| 521 | + if (!isPathUnderParent(request.user.directories.chats, directoryPath)) { | |
| 522 | + return response.sendStatus(400); | |
| 523 | + } | |
| 517 | 524 | const chatDirExists = fs.existsSync(directoryPath); |
| 518 | 525 | |
| 519 | 526 | //if no chat dir for the character is found, make one with the character name |
| @@ -545,6 +552,9 @@ router.post('/rename', validateAvatarUrlMiddleware, async function (request, res | ||
| 545 | 552 | const pathToFolder = request.body.is_group |
| 546 | 553 | ? request.user.directories.groupChats |
| 547 | 554 | : path.join(request.user.directories.chats, String(request.body.avatar_url).replace('.png', '')); |
| 555 | + if (!request.body.is_group && !isPathUnderParent(request.user.directories.chats, pathToFolder)) { | |
| 556 | + return response.sendStatus(400); | |
| 557 | + } | |
| 548 | 558 | const pathToOriginalFile = path.join(pathToFolder, sanitize(request.body.original_file)); |
| 549 | 559 | const pathToRenamedFile = path.join(pathToFolder, sanitize(request.body.renamed_file)); |
| 550 | 560 | const sanitizedFileName = path.parse(pathToRenamedFile).name; |
| @@ -575,6 +585,9 @@ router.post('/delete', validateAvatarUrlMiddleware, function (request, response) | ||
| 575 | 585 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 576 | 586 | const chatFileName = String(request.body.chatfile); |
| 577 | 587 | const chatFilePath = path.join(request.user.directories.chats, dirName, sanitize(chatFileName)); |
| 588 | + if (!isPathUnderParent(request.user.directories.chats, chatFilePath)) { | |
| 589 | + return response.sendStatus(400); | |
| 590 | + } | |
| 578 | 591 | //Return success if the file was deleted. |
| 579 | 592 | if (tryDeleteFile(chatFilePath)) { |
| 580 | 593 | return response.send({ ok: true }); |
| @@ -595,7 +608,10 @@ router.post('/export', validateAvatarUrlMiddleware, async function (request, res | ||
| 595 | 608 | const pathToFolder = request.body.is_group |
| 596 | 609 | ? request.user.directories.groupChats |
| 597 | 610 | : path.join(request.user.directories.chats, String(request.body.avatar_url).replace('.png', '')); |
| 598 | 611 | letconst filename = path.join(pathToFolder, sanitize(request.body.file)); |
| 612 | + if (!request.body.is_group && !isPathUnderParent(request.user.directories.chats, filename)) { | |
| 613 | + return response.sendStatus(400); | |
| 614 | + } | |
| 599 | 615 | let exportfilename = request.body.exportfilename; |
| 600 | 616 | if (!fs.existsSync(filename)) { |
| 601 | 617 | const errorMessage = { |
| @@ -690,6 +706,11 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 690 | 706 | return response.sendStatus(400); |
| 691 | 707 | } |
| 692 | 708 | |
| 709 | + const directoryPath = path.join(request.user.directories.chats, avatarUrl); | |
| 710 | + if (!isPathUnderParent(request.user.directories.chats, directoryPath)) { | |
| 711 | + return response.sendStatus(400); | |
| 712 | + } | |
| 713 | + | |
| 693 | 714 | try { |
| 694 | 715 | const pathToUpload = path.join(request.file.destination, request.file.filename); |
| 695 | 716 | const data = fs.readFileSync(pathToUpload, 'utf8'); |
| @@ -718,7 +739,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 718 | 739 | |
| 719 | 740 | const handleChat = (chat) => { |
| 720 | 741 | const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`; |
| 721 | 742 | const filePath = path.join(request.user.directories.chats, avatarUrldirectoryPath, fileName); |
| 722 | 743 | fileNames.push(fileName); |
| 723 | 744 | writeFileAtomicSync(filePath, chat, 'utf8'); |
| 724 | 745 | }; |
| @@ -757,7 +778,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 757 | 778 | } |
| 758 | 779 | |
| 759 | 780 | const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`; |
| 760 | 781 | const filePath = path.join(request.user.directories.chats, avatarUrldirectoryPath, fileName); |
| 761 | 782 | fileNames.push(fileName); |
| 762 | 783 | if (flattenedChat !== data) { |
| 763 | 784 | writeFileAtomicSync(filePath, flattenedChat, 'utf8'); |