diff --git a/src/endpoints/avatars.js b/src/endpoints/avatars.js index 73b995ffb..f84527670 100644 --- a/src/endpoints/avatars.js +++ b/src/endpoints/avatars.js @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; import { jsonParser, urlencodedParser } from '../express-common.js'; import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; import { getImages, tryParse } from '../util.js'; +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; export const router = express.Router(); @@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) { response.send(JSON.stringify(images)); }); -router.post('/delete', jsonParser, function (request, response) { +router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) { if (!request.body) return response.sendStatus(400); if (request.body.avatar !== sanitize(request.body.avatar)) { diff --git a/src/endpoints/backgrounds.js b/src/endpoints/backgrounds.js index 0638415e6..13705fa7a 100644 --- a/src/endpoints/backgrounds.js +++ b/src/endpoints/backgrounds.js @@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename'; import { jsonParser, urlencodedParser } from '../express-common.js'; import { invalidateThumbnail } from './thumbnails.js'; import { getImages } from '../util.js'; +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; export const router = express.Router(); @@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) { response.send(JSON.stringify(images)); }); -router.post('/delete', jsonParser, function (request, response) { +router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) { if (!request.body) return response.sendStatus(400); if (request.body.bg !== sanitize(request.body.bg)) { diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 2aa2cca10..c7a2a02d4 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -14,6 +14,7 @@ import jimp from 'jimp'; import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; import { jsonParser, urlencodedParser } from '../express-common.js'; +import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js'; import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; import { TavernCardValidator } from '../validator/TavernCardValidator.js'; import { parse, write } from '../character-card-parser.js'; @@ -756,7 +757,7 @@ router.post('/create', urlencodedParser, async function (request, response) { } }); -router.post('/rename', jsonParser, async function (request, response) { +router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body.avatar_url || !request.body.new_name) { return response.sendStatus(400); } @@ -803,7 +804,7 @@ router.post('/rename', jsonParser, async function (request, response) { } }); -router.post('/edit', urlencodedParser, async function (request, response) { +router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body) { console.error('Error: no response body detected'); response.status(400).send('Error: no response body detected'); @@ -852,7 +853,7 @@ router.post('/edit', urlencodedParser, async function (request, response) { * @param {Object} response - The HTTP response object. * @returns {void} */ -router.post('/edit-attribute', jsonParser, async function (request, response) { +router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { console.log(request.body); if (!request.body) { console.error('Error: no response body detected'); @@ -898,7 +899,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) { * * @returns {void} * */ -router.post('/merge-attributes', jsonParser, async function (request, response) { +router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) { try { const update = request.body; const avatarPath = path.join(request.user.directories.characters, update.avatar); @@ -929,7 +930,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response) } }); -router.post('/delete', jsonParser, async function (request, response) { +router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body || !request.body.avatar_url) { return response.sendStatus(400); } @@ -992,7 +993,7 @@ router.post('/all', jsonParser, async function (request, response) { } }); -router.post('/get', jsonParser, async function (request, response) { +router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { try { if (!request.body) return response.sendStatus(400); const item = request.body.avatar_url; @@ -1011,7 +1012,7 @@ router.post('/get', jsonParser, async function (request, response) { } }); -router.post('/chats', jsonParser, async function (request, response) { +router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body) return response.sendStatus(400); const characterDirectory = (request.body.avatar_url).replace('.png', ''); @@ -1160,7 +1161,7 @@ router.post('/import', urlencodedParser, async function (request, response) { } }); -router.post('/duplicate', jsonParser, async function (request, response) { +router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { try { if (!request.body.avatar_url) { console.log('avatar URL not found in request body'); @@ -1207,7 +1208,7 @@ router.post('/duplicate', jsonParser, async function (request, response) { } }); -router.post('/export', jsonParser, async function (request, response) { +router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { try { if (!request.body.format || !request.body.avatar_url) { return response.sendStatus(400); diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 2df194797..c3bab87ac 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; import _ from 'lodash'; import { jsonParser, urlencodedParser } from '../express-common.js'; +import validateAvatarUrlMiddleware from '../middleware/validateFileName.js'; import { getConfigValue, humanizedISO8601DateTime, @@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) { export const router = express.Router(); -router.post('/save', jsonParser, function (request, response) { +router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) { try { const directoryName = String(request.body.avatar_url).replace('.png', ''); const chatData = request.body.chat; @@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) { } }); -router.post('/get', jsonParser, function (request, response) { +router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) { try { const dirName = String(request.body.avatar_url).replace('.png', ''); const directoryPath = path.join(request.user.directories.chats, dirName); @@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) { }); -router.post('/rename', jsonParser, async function (request, response) { +router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body || !request.body.original_file || !request.body.renamed_file) { return response.sendStatus(400); } @@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) { return response.send({ ok: true, sanitizedFileName }); }); -router.post('/delete', jsonParser, function (request, response) { +router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) { const dirName = String(request.body.avatar_url).replace('.png', ''); const fileName = String(request.body.chatfile); const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); @@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) { return response.send('ok'); }); -router.post('/export', jsonParser, async function (request, response) { +router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { return response.sendStatus(400); } @@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) { } }); -router.post('/import', urlencodedParser, function (request, response) { +router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) { if (!request.body) return response.sendStatus(400); const format = request.body.file_type; @@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => { return response.send({ ok: true }); }); -router.post('/search', jsonParser, function (request, response) { +router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) { try { const { query, avatar_url, group_id } = request.body; let chatFiles = []; diff --git a/src/endpoints/settings.js b/src/endpoints/settings.js index 3cc7e3bec..4df4978cc 100644 --- a/src/endpoints/settings.js +++ b/src/endpoints/settings.js @@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js'; import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; import { jsonParser } from '../express-common.js'; import { getAllUserHandles, getUserDirectories } from '../users.js'; +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); @@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => { } }); -router.post('/load-snapshot', jsonParser, async (request, response) => { +router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { try { const userFilesPattern = getFilePrefix(request.user.profile.handle); @@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => { } }); -router.post('/restore-snapshot', jsonParser, async (request, response) => { +router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { try { const userFilesPattern = getFilePrefix(request.user.profile.handle); diff --git a/src/middleware/validateFileName.js b/src/middleware/validateFileName.js new file mode 100644 index 000000000..ccfb0e88d --- /dev/null +++ b/src/middleware/validateFileName.js @@ -0,0 +1,34 @@ +import path from 'node:path'; + +/** + * Gets a middleware function that validates the field in the request body. + * @param {string} fieldName Field name + * @returns {import('express').RequestHandler} Middleware function + */ +export function getFileNameValidationFunction(fieldName) { + /** + * Validates the field in the request body. + * @param {import('express').Request} req Request object + * @param {import('express').Response} res Response object + * @param {import('express').NextFunction} next Next middleware + */ + return function validateAvatarUrlMiddleware(req, res, next) { + if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') { + const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/; + if (forbiddenRegExp.test(req.body[fieldName])) { + console.error('An error occurred while validating the request body', { + handle: req.user.profile.handle, + path: req.originalUrl, + field: fieldName, + value: req.body[fieldName], + }); + return res.sendStatus(400); + } + } + + next(); + }; +} + +const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url'); +export default avatarUrlValidationFunction;