Validate avatar_url field with a middleware (#3314) * Validate avatar_url field with a middleware * Fix validating wrong endpoint

0937f44f397eaca9c21b5a8334aca54dd3f506cc

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +59 -20Ignore whitespace
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import { jsonParser, urlencodedParser } from '../express-common.js';
1010import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1111import { getImages, tryParse } from '../util.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314export const router = express.Router();
1415
@@ -17,7 +18,7 @@ router.post('/get', jsonParser, function (request, response) {
1718 response.send(JSON.stringify(images));
1819});
1920
2021router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
2122 if (!request.body) return response.sendStatus(400);
2223
2324 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/backgrounds.js+2 -1
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
77import { jsonParser, urlencodedParser } from '../express-common.js';
88import { invalidateThumbnail } from './thumbnails.js';
99import { getImages } from '../util.js';
10+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
1112export const router = express.Router();
1213
@@ -15,7 +16,7 @@ router.post('/all', jsonParser, function (request, response) {
1516 response.send(JSON.stringify(images));
1617});
1718
1819router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
1920 if (!request.body) return response.sendStatus(400);
2021
2122 if (request.body.bg !== sanitize(request.body.bg)) {
src/endpoints/characters.js+10 -9
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
1515import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
1616import { jsonParser, urlencodedParser } from '../express-common.js';
17+import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
1718import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
1819import { TavernCardValidator } from '../validator/TavernCardValidator.js';
1920import { parse, write } from '../character-card-parser.js';
@@ -756,7 +757,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756757 }
757758});
758759
759760router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
760761 if (!request.body.avatar_url || !request.body.new_name) {
761762 return response.sendStatus(400);
762763 }
@@ -803,7 +804,7 @@ router.post('/rename', jsonParser, async function (request, response) {
803804 }
804805});
805806
806807router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async function (request, response) {
807808 if (!request.body) {
808809 console.error('Error: no response body detected');
809810 response.status(400).send('Error: no response body detected');
@@ -852,7 +853,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
852853 * @param {Object} response - The HTTP response object.
853854 * @returns {void}
854855 */
855856router.post('/edit-attribute', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
856857 console.log(request.body);
857858 if (!request.body) {
858859 console.error('Error: no response body detected');
@@ -898,7 +899,7 @@ router.post('/edit-attribute', jsonParser, async function (request, response) {
898899 *
899900 * @returns {void}
900901 * */
901902router.post('/merge-attributes', jsonParser, getFileNameValidationFunction('avatar'), async function (request, response) {
902903 try {
903904 const update = request.body;
904905 const avatarPath = path.join(request.user.directories.characters, update.avatar);
@@ -929,7 +930,7 @@ router.post('/merge-attributes', jsonParser, async function (request, response)
929930 }
930931});
931932
932933router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
933934 if (!request.body || !request.body.avatar_url) {
934935 return response.sendStatus(400);
935936 }
@@ -992,7 +993,7 @@ router.post('/all', jsonParser, async function (request, response) {
992993 }
993994});
994995
995996router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
996997 try {
997998 if (!request.body) return response.sendStatus(400);
998999 const item = request.body.avatar_url;
@@ -1011,7 +1012,7 @@ router.post('/get', jsonParser, async function (request, response) {
10111012 }
10121013});
10131014
10141015router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
10151016 if (!request.body) return response.sendStatus(400);
10161017
10171018 const characterDirectory = (request.body.avatar_url).replace('.png', '');
@@ -1160,7 +1161,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
11601161 }
11611162});
11621163
11631164router.post('/duplicate', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
11641165 try {
11651166 if (!request.body.avatar_url) {
11661167 console.log('avatar URL not found in request body');
@@ -1207,7 +1208,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
12071208 }
12081209});
12091210
12101211router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
12111212 try {
12121213 if (!request.body.format || !request.body.avatar_url) {
12131214 return response.sendStatus(400);
src/endpoints/chats.js+8 -7
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import _ from 'lodash';
1010
1111import { jsonParser, urlencodedParser } from '../express-common.js';
12+import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
1213import {
1314 getConfigValue,
1415 humanizedISO8601DateTime,
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295296export const router = express.Router();
296297
297298router.post('/save', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
298299 try {
299300 const directoryName = String(request.body.avatar_url).replace('.png', '');
300301 const chatData = request.body.chat;
@@ -310,7 +311,7 @@ router.post('/save', jsonParser, function (request, response) {
310311 }
311312});
312313
313314router.post('/get', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
314315 try {
315316 const dirName = String(request.body.avatar_url).replace('.png', '');
316317 const directoryPath = path.join(request.user.directories.chats, dirName);
@@ -347,7 +348,7 @@ router.post('/get', jsonParser, function (request, response) {
347348});
348349
349350
350351router.post('/rename', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
351352 if (!request.body || !request.body.original_file || !request.body.renamed_file) {
352353 return response.sendStatus(400);
353354 }
@@ -372,7 +373,7 @@ router.post('/rename', jsonParser, async function (request, response) {
372373 return response.send({ ok: true, sanitizedFileName });
373374});
374375
375376router.post('/delete', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
376377 const dirName = String(request.body.avatar_url).replace('.png', '');
377378 const fileName = String(request.body.chatfile);
378379 const filePath = path.join(request.user.directories.chats, dirName, sanitize(fileName));
@@ -388,7 +389,7 @@ router.post('/delete', jsonParser, function (request, response) {
388389 return response.send('ok');
389390});
390391
391392router.post('/export', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
392393 if (!request.body.file || (!request.body.avatar_url && request.body.is_group === false)) {
393394 return response.sendStatus(400);
394395 }
@@ -478,7 +479,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
478479 }
479480});
480481
481482router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482483 if (!request.body) return response.sendStatus(400);
483484
484485 const format = request.body.file_type;
@@ -626,7 +627,7 @@ router.post('/group/save', jsonParser, (request, response) => {
626627 return response.send({ ok: true });
627628});
628629
629630router.post('/search', jsonParser, validateAvatarUrlMiddleware, function (request, response) {
630631 try {
631632 const { query, avatar_url, group_id } = request.body;
632633 let chatFiles = [];
src/endpoints/settings.js+3 -2
@@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js';
99import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
1010import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
12+import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
1314const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true);
1415const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true);
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296297 }
297298});
298299
299300router.post('/load-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
300301 try {
301302 const userFilesPattern = getFilePrefix(request.user.profile.handle);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330331 }
331332});
332333
333334router.post('/restore-snapshot', jsonParser, getFileNameValidationFunction('name'), async (request, response) => {
334335 try {
335336 const userFilesPattern = getFilePrefix(request.user.profile.handle);
336337
src/middleware/validateFileName.js+34 -0
@@ -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;