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