Validate avatar_url field with a middleware (#3314) * Validate avatar_url field with a middleware * Fix validating wrong endpoint
Signed| @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | ||
| 9 | 9 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 10 | 10 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 11 | 11 | import { getImages, tryParse } from '../util.js'; |
| 12 | +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | |
| 12 | 13 | |
| 13 | 14 | export const router = express.Router(); |
| 14 | 15 | |
| @@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) { | ||
| 17 | 18 | response.send(JSON.stringify(images)); |
| 18 | 19 | }); |
| 19 | 20 | |
| 20 | 21 | router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) { |
| 21 | 22 | if (!request.body) return response.sendStatus(400); |
| 22 | 23 | |
| 23 | 24 | if (request.body.avatar !== sanitize(request.body.avatar)) { |
| @@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename'; | ||
| 7 | 7 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 8 | 8 | import { invalidateThumbnail } from './thumbnails.js'; |
| 9 | 9 | import { getImages } from '../util.js'; |
| 10 | +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | |
| 10 | 11 | |
| 11 | 12 | export const router = express.Router(); |
| 12 | 13 | |
| @@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) { | ||
| 15 | 16 | response.send(JSON.stringify(images)); |
| 16 | 17 | }); |
| 17 | 18 | |
| 18 | 19 | router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) { |
| 19 | 20 | if (!request.body) return response.sendStatus(400); |
| 20 | 21 | |
| 21 | 22 | if (request.body.bg !== sanitize(request.body.bg)) { |
| @@ -14,6 +14,7 @@ import jimp from 'jimp'; | ||
| 14 | 14 | |
| 15 | 15 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 16 | 16 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 17 | +import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js'; | |
| 17 | 18 | import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; |
| 18 | 19 | import { TavernCardValidator } from '../validator/TavernCardValidator.js'; |
| 19 | 20 | import { parse, write } from '../character-card-parser.js'; |
| @@ -756,7 +757,7 @@ router.post('/create', urlencodedParser, async function (request, response) { | ||
| 756 | 757 | } |
| 757 | 758 | }); |
| 758 | 759 | |
| 759 | 760 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 760 | 761 | if (!request.body.avatar_url || !request.body.new_name) { |
| 761 | 762 | return response.sendStatus(400); |
| 762 | 763 | } |
| @@ -803,7 +804,7 @@ router.post('/rename', jsonParser, async function (request, response) { | ||
| 803 | 804 | } |
| 804 | 805 | }); |
| 805 | 806 | |
| 806 | 807 | router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 807 | 808 | if (!request.body) { |
| 808 | 809 | console.error('Error: no response body detected'); |
| 809 | 810 | response.status(400).send('Error: no response body detected'); |
| @@ -852,7 +853,7 @@ router.post('/edit', urlencodedParser, async function (request, response) { | ||
| 852 | 853 | * @param {Object} response - The HTTP response object. |
| 853 | 854 | * @returns {void} |
| 854 | 855 | */ |
| 855 | 856 | router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 856 | 857 | console.log(request.body); |
| 857 | 858 | if (!request.body) { |
| 858 | 859 | console.error('Error: no response body detected'); |
| @@ -898,7 +899,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) { | ||
| 898 | 899 | * |
| 899 | 900 | * @returns {void} |
| 900 | 901 | * */ |
| 901 | 902 | router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) { |
| 902 | 903 | try { |
| 903 | 904 | const update = request.body; |
| 904 | 905 | const avatarPath = path.join(request.user.directories.characters, update.avatar); |
| @@ -929,7 +930,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response) | ||
| 929 | 930 | } |
| 930 | 931 | }); |
| 931 | 932 | |
| 932 | 933 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 933 | 934 | if (!request.body || !request.body.avatar_url) { |
| 934 | 935 | return response.sendStatus(400); |
| 935 | 936 | } |
| @@ -992,7 +993,7 @@ router.post('/all', jsonParser, async function (request, response) { | ||
| 992 | 993 | } |
| 993 | 994 | }); |
| 994 | 995 | |
| 995 | 996 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 996 | 997 | try { |
| 997 | 998 | if (!request.body) return response.sendStatus(400); |
| 998 | 999 | const item = request.body.avatar_url; |
| @@ -1011,7 +1012,7 @@ router.post('/get', jsonParser, async function (request, response) { | ||
| 1011 | 1012 | } |
| 1012 | 1013 | }); |
| 1013 | 1014 | |
| 1014 | 1015 | router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1015 | 1016 | if (!request.body) return response.sendStatus(400); |
| 1016 | 1017 | |
| 1017 | 1018 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); |
| @@ -1160,7 +1161,7 @@ router.post('/import', urlencodedParser, async function (request, response) { | ||
| 1160 | 1161 | } |
| 1161 | 1162 | }); |
| 1162 | 1163 | |
| 1163 | 1164 | router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1164 | 1165 | try { |
| 1165 | 1166 | if (!request.body.avatar_url) { |
| 1166 | 1167 | console.log('avatar URL not found in request body'); |
| @@ -1207,7 +1208,7 @@ router.post('/duplicate', jsonParser, async function (request, response) { | ||
| 1207 | 1208 | } |
| 1208 | 1209 | }); |
| 1209 | 1210 | |
| 1210 | 1211 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1211 | 1212 | try { |
| 1212 | 1213 | if (!request.body.format || !request.body.avatar_url) { |
| 1213 | 1214 | return response.sendStatus(400); |
| @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | ||
| 9 | 9 | import _ from 'lodash'; |
| 10 | 10 | |
| 11 | 11 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 12 | +import validateAvatarUrlMiddleware from '../middleware/validateFileName.js'; | |
| 12 | 13 | import { |
| 13 | 14 | getConfigValue, |
| 14 | 15 | humanizedISO8601DateTime, |
| @@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) { | ||
| 294 | 295 | |
| 295 | 296 | export const router = express.Router(); |
| 296 | 297 | |
| 297 | 298 | router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 298 | 299 | try { |
| 299 | 300 | const directoryName = String(request.body.avatar_url).replace('.png', ''); |
| 300 | 301 | const chatData = request.body.chat; |
| @@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) { | ||
| 310 | 311 | } |
| 311 | 312 | }); |
| 312 | 313 | |
| 313 | 314 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 314 | 315 | try { |
| 315 | 316 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 316 | 317 | const directoryPath = path.join(request.user.directories.chats, dirName); |
| @@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) { | ||
| 347 | 348 | }); |
| 348 | 349 | |
| 349 | 350 | |
| 350 | 351 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 351 | 352 | if (!request.body || !request.body.original_file || !request.body.renamed_file) { |
| 352 | 353 | return response.sendStatus(400); |
| 353 | 354 | } |
| @@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) { | ||
| 372 | 373 | return response.send({ ok: true, sanitizedFileName }); |
| 373 | 374 | }); |
| 374 | 375 | |
| 375 | 376 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 376 | 377 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 377 | 378 | const fileName = String(request.body.chatfile); |
| 378 | 379 | const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); |
| @@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) { | ||
| 388 | 389 | return response.send('ok'); |
| 389 | 390 | }); |
| 390 | 391 | |
| 391 | 392 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 392 | 393 | if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { |
| 393 | 394 | return response.sendStatus(400); |
| 394 | 395 | } |
| @@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) { | ||
| 478 | 479 | } |
| 479 | 480 | }); |
| 480 | 481 | |
| 481 | 482 | router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) { |
| 482 | 483 | if (!request.body) return response.sendStatus(400); |
| 483 | 484 | |
| 484 | 485 | const format = request.body.file_type; |
| @@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => { | ||
| 626 | 627 | return response.send({ ok: true }); |
| 627 | 628 | }); |
| 628 | 629 | |
| 629 | 630 | router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 630 | 631 | try { |
| 631 | 632 | const { query, avatar_url, group_id } = request.body; |
| 632 | 633 | let chatFiles = []; |
| @@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js'; | ||
| 9 | 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; |
| 10 | 10 | import { jsonParser } from '../express-common.js'; |
| 11 | 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; |
| 12 | +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | |
| 12 | 13 | |
| 13 | 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); |
| 14 | 15 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); |
| @@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => { | ||
| 296 | 297 | } |
| 297 | 298 | }); |
| 298 | 299 | |
| 299 | 300 | router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 300 | 301 | try { |
| 301 | 302 | const userFilesPattern = getFilePrefix(request.user.profile.handle); |
| 302 | 303 | |
| @@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => { | ||
| 330 | 331 | } |
| 331 | 332 | }); |
| 332 | 333 | |
| 333 | 334 | router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 334 | 335 | try { |
| 335 | 336 | const userFilesPattern = getFilePrefix(request.user.profile.handle); |
| 336 | 337 | |
| @@ -0,0 +1,34 @@ | ||
| 1 | +import path from 'node:path'; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Gets a middleware function that validates the field in the request body. | |
| 5 | + * @param {string} fieldName Field name | |
| 6 | + * @returns {import('express').RequestHandler} Middleware function | |
| 7 | + */ | |
| 8 | +export function getFileNameValidationFunction(fieldName) { | |
| 9 | + /** | |
| 10 | + * Validates the field in the request body. | |
| 11 | + * @param {import('express').Request} req Request object | |
| 12 | + * @param {import('express').Response} res Response object | |
| 13 | + * @param {import('express').NextFunction} next Next middleware | |
| 14 | + */ | |
| 15 | + return function validateAvatarUrlMiddleware(req, res, next) { | |
| 16 | + if (req.body && fieldName in req.body && typeof req.body[fieldName] === 'string') { | |
| 17 | + const forbiddenRegExp = path.sep === '/' ? /[/\x00]/ : /[/\x00\\]/; | |
| 18 | + if (forbiddenRegExp.test(req.body[fieldName])) { | |
| 19 | + console.error('An error occurred while validating the request body', { | |
| 20 | + handle: req.user.profile.handle, | |
| 21 | + path: req.originalUrl, | |
| 22 | + field: fieldName, | |
| 23 | + value: req.body[fieldName], | |
| 24 | + }); | |
| 25 | + return res.sendStatus(400); | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 29 | + next(); | |
| 30 | + }; | |
| 31 | +} | |
| 32 | + | |
| 33 | +const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url'); | |
| 34 | +export default avatarUrlValidationFunction; | |