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 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 9 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 10 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; | 10 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 11 | import { getImages, tryParse } from '../util.js'; | 11 | import { getImages, tryParse } from '../util.js'; |
| 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 12 | 13 | ||
| 13 | export const router = express.Router(); | 14 | export const router = express.Router(); |
| 14 | 15 | ||
| @@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) { | |||
| 17 | response.send(JSON.stringify(images)); | 18 | response.send(JSON.stringify(images)); |
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | router.post('/delete', jsonParser, function (request, response) { | 21 | router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) { |
| 21 | if (!request.body) return response.sendStatus(400); | 22 | if (!request.body) return response.sendStatus(400); |
| 22 | 23 | ||
| 23 | if (request.body.avatar !== sanitize(request.body.avatar)) { | 24 | if (request.body.avatar !== sanitize(request.body.avatar)) { |
| @@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename'; | |||
| 7 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 7 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 8 | import { invalidateThumbnail } from './thumbnails.js'; | 8 | import { invalidateThumbnail } from './thumbnails.js'; |
| 9 | import { getImages } from '../util.js'; | 9 | import { getImages } from '../util.js'; |
| 10 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 10 | 11 | ||
| 11 | export const router = express.Router(); | 12 | export const router = express.Router(); |
| 12 | 13 | ||
| @@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) { | |||
| 15 | response.send(JSON.stringify(images)); | 16 | response.send(JSON.stringify(images)); |
| 16 | }); | 17 | }); |
| 17 | 18 | ||
| 18 | router.post('/delete', jsonParser, function (request, response) { | 19 | router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) { |
| 19 | if (!request.body) return response.sendStatus(400); | 20 | if (!request.body) return response.sendStatus(400); |
| 20 | 21 | ||
| 21 | if (request.body.bg !== sanitize(request.body.bg)) { | 22 | if (request.body.bg !== sanitize(request.body.bg)) { |
| @@ -14,6 +14,7 @@ import jimp from 'jimp'; | |||
| 14 | 14 | ||
| 15 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; | 15 | import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js'; |
| 16 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 16 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 17 | import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 17 | import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; | 18 | import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js'; |
| 18 | import { TavernCardValidator } from '../validator/TavernCardValidator.js'; | 19 | import { TavernCardValidator } from '../validator/TavernCardValidator.js'; |
| 19 | import { parse, write } from '../character-card-parser.js'; | 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 | router.post('/rename', jsonParser, async function (request, response) { | 760 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 760 | if (!request.body.avatar_url || !request.body.new_name) { | 761 | if (!request.body.avatar_url || !request.body.new_name) { |
| 761 | return response.sendStatus(400); | 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 | router.post('/edit', urlencodedParser, async function (request, response) { | 807 | router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 807 | if (!request.body) { | 808 | if (!request.body) { |
| 808 | console.error('Error: no response body detected'); | 809 | console.error('Error: no response body detected'); |
| 809 | response.status(400).send('Error: no response body detected'); | 810 | response.status(400).send('Error: no response body detected'); |
| @@ -852,7 +853,7 @@ router.post('/edit', urlencodedParser, async function (request, response) { | |||
| 852 | * @param {Object} response - The HTTP response object. | 853 | * @param {Object} response - The HTTP response object. |
| 853 | * @returns {void} | 854 | * @returns {void} |
| 854 | */ | 855 | */ |
| 855 | router.post('/edit-attribute', jsonParser, async function (request, response) { | 856 | router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 856 | console.log(request.body); | 857 | console.log(request.body); |
| 857 | if (!request.body) { | 858 | if (!request.body) { |
| 858 | console.error('Error: no response body detected'); | 859 | console.error('Error: no response body detected'); |
| @@ -898,7 +899,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) { | |||
| 898 | * | 899 | * |
| 899 | * @returns {void} | 900 | * @returns {void} |
| 900 | * */ | 901 | * */ |
| 901 | router.post('/merge-attributes', jsonParser, async function (request, response) { | 902 | router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) { |
| 902 | try { | 903 | try { |
| 903 | const update = request.body; | 904 | const update = request.body; |
| 904 | const avatarPath = path.join(request.user.directories.characters, update.avatar); | 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 | router.post('/delete', jsonParser, async function (request, response) { | 933 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 933 | if (!request.body || !request.body.avatar_url) { | 934 | if (!request.body || !request.body.avatar_url) { |
| 934 | return response.sendStatus(400); | 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 | router.post('/get', jsonParser, async function (request, response) { | 996 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 996 | try { | 997 | try { |
| 997 | if (!request.body) return response.sendStatus(400); | 998 | if (!request.body) return response.sendStatus(400); |
| 998 | const item = request.body.avatar_url; | 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 | router.post('/chats', jsonParser, async function (request, response) { | 1015 | router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1015 | if (!request.body) return response.sendStatus(400); | 1016 | if (!request.body) return response.sendStatus(400); |
| 1016 | 1017 | ||
| 1017 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); | 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 | router.post('/duplicate', jsonParser, async function (request, response) { | 1164 | router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1164 | try { | 1165 | try { |
| 1165 | if (!request.body.avatar_url) { | 1166 | if (!request.body.avatar_url) { |
| 1166 | console.log('avatar URL not found in request body'); | 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 | router.post('/export', jsonParser, async function (request, response) { | 1211 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 1211 | try { | 1212 | try { |
| 1212 | if (!request.body.format || !request.body.avatar_url) { | 1213 | if (!request.body.format || !request.body.avatar_url) { |
| 1213 | return response.sendStatus(400); | 1214 | return response.sendStatus(400); |
| @@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 9 | import _ from 'lodash'; | 9 | import _ from 'lodash'; |
| 10 | 10 | ||
| 11 | import { jsonParser, urlencodedParser } from '../express-common.js'; | 11 | import { jsonParser, urlencodedParser } from '../express-common.js'; |
| 12 | import validateAvatarUrlMiddleware from '../middleware/validateFileName.js'; | ||
| 12 | import { | 13 | import { |
| 13 | getConfigValue, | 14 | getConfigValue, |
| 14 | humanizedISO8601DateTime, | 15 | humanizedISO8601DateTime, |
| @@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) { | |||
| 294 | 295 | ||
| 295 | export const router = express.Router(); | 296 | export const router = express.Router(); |
| 296 | 297 | ||
| 297 | router.post('/save', jsonParser, function (request, response) { | 298 | router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 298 | try { | 299 | try { |
| 299 | const directoryName = String(request.body.avatar_url).replace('.png', ''); | 300 | const directoryName = String(request.body.avatar_url).replace('.png', ''); |
| 300 | const chatData = request.body.chat; | 301 | const chatData = request.body.chat; |
| @@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) { | |||
| 310 | } | 311 | } |
| 311 | }); | 312 | }); |
| 312 | 313 | ||
| 313 | router.post('/get', jsonParser, function (request, response) { | 314 | router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 314 | try { | 315 | try { |
| 315 | const dirName = String(request.body.avatar_url).replace('.png', ''); | 316 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 316 | const directoryPath = path.join(request.user.directories.chats, dirName); | 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 | router.post('/rename', jsonParser, async function (request, response) { | 351 | router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 351 | if (!request.body || !request.body.original_file || !request.body.renamed_file) { | 352 | if (!request.body || !request.body.original_file || !request.body.renamed_file) { |
| 352 | return response.sendStatus(400); | 353 | return response.sendStatus(400); |
| 353 | } | 354 | } |
| @@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) { | |||
| 372 | return response.send({ ok: true, sanitizedFileName }); | 373 | return response.send({ ok: true, sanitizedFileName }); |
| 373 | }); | 374 | }); |
| 374 | 375 | ||
| 375 | router.post('/delete', jsonParser, function (request, response) { | 376 | router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 376 | const dirName = String(request.body.avatar_url).replace('.png', ''); | 377 | const dirName = String(request.body.avatar_url).replace('.png', ''); |
| 377 | const fileName = String(request.body.chatfile); | 378 | const fileName = String(request.body.chatfile); |
| 378 | const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); | 379 | const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName)); |
| @@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) { | |||
| 388 | return response.send('ok'); | 389 | return response.send('ok'); |
| 389 | }); | 390 | }); |
| 390 | 391 | ||
| 391 | router.post('/export', jsonParser, async function (request, response) { | 392 | router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) { |
| 392 | if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { | 393 | if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) { |
| 393 | return response.sendStatus(400); | 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 | router.post('/import', urlencodedParser, function (request, response) { | 482 | router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) { |
| 482 | if (!request.body) return response.sendStatus(400); | 483 | if (!request.body) return response.sendStatus(400); |
| 483 | 484 | ||
| 484 | const format = request.body.file_type; | 485 | const format = request.body.file_type; |
| @@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => { | |||
| 626 | return response.send({ ok: true }); | 627 | return response.send({ ok: true }); |
| 627 | }); | 628 | }); |
| 628 | 629 | ||
| 629 | router.post('/search', jsonParser, function (request, response) { | 630 | router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) { |
| 630 | try { | 631 | try { |
| 631 | const { query, avatar_url, group_id } = request.body; | 632 | const { query, avatar_url, group_id } = request.body; |
| 632 | let chatFiles = []; | 633 | let chatFiles = []; |
| @@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js'; | |||
| 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; | 9 | import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js'; |
| 10 | import { jsonParser } from '../express-common.js'; | 10 | import { jsonParser } from '../express-common.js'; |
| 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; | 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; |
| 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 12 | 13 | ||
| 13 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); | 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); |
| 14 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); | 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 | router.post('/load-snapshot', jsonParser, async (request, response) => { | 300 | router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 300 | try { | 301 | try { |
| 301 | const userFilesPattern = getFilePrefix(request.user.profile.handle); | 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 | router.post('/restore-snapshot', jsonParser, async (request, response) => { | 334 | router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => { |
| 334 | try { | 335 | try { |
| 335 | const userFilesPattern = getFilePrefix(request.user.profile.handle); | 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; | ||