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 -20Showing whitespace changes
src/endpoints/avatars.js+2 -1
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import { jsonParser, urlencodedParser } from '../express-common.js';9import { jsonParser, urlencodedParser } from '../express-common.js';
10import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';10import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
11import { getImages, tryParse } from '../util.js';11import { getImages, tryParse } from '../util.js';
12import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
13export const router = express.Router();14export const router = express.Router();
1415
@@ -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});
1920
20router.post('/delete', jsonParser, function (request, response) {21router.post('/delete', jsonParser, getFileNameValidationFunction('avatar'), function (request, response) {
21 if (!request.body) return response.sendStatus(400);22 if (!request.body) return response.sendStatus(400);
2223
23 if (request.body.avatar !== sanitize(request.body.avatar)) {24 if (request.body.avatar !== sanitize(request.body.avatar)) {
src/endpoints/backgrounds.js+2 -1
@@ -7,6 +7,7 @@ import sanitize from 'sanitize-filename';
7import { jsonParser, urlencodedParser } from '../express-common.js';7import { jsonParser, urlencodedParser } from '../express-common.js';
8import { invalidateThumbnail } from './thumbnails.js';8import { invalidateThumbnail } from './thumbnails.js';
9import { getImages } from '../util.js';9import { getImages } from '../util.js';
10import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1011
11export const router = express.Router();12export const router = express.Router();
1213
@@ -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});
1718
18router.post('/delete', jsonParser, function (request, response) {19router.post('/delete', jsonParser, getFileNameValidationFunction('bg'), function (request, response) {
19 if (!request.body) return response.sendStatus(400);20 if (!request.body) return response.sendStatus(400);
2021
21 if (request.body.bg !== sanitize(request.body.bg)) {22 if (request.body.bg !== sanitize(request.body.bg)) {
src/endpoints/characters.js+10 -9
@@ -14,6 +14,7 @@ import jimp from 'jimp';
1414
15import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';15import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
16import { jsonParser, urlencodedParser } from '../express-common.js';16import { jsonParser, urlencodedParser } from '../express-common.js';
17import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
17import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';18import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue } from '../util.js';
18import { TavernCardValidator } from '../validator/TavernCardValidator.js';19import { TavernCardValidator } from '../validator/TavernCardValidator.js';
19import { parse, write } from '../character-card-parser.js';20import { parse, write } from '../character-card-parser.js';
@@ -756,7 +757,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
756 }757 }
757});758});
758759
759router.post('/rename', jsonParser, async function (request, response) {760router.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});
805806
806router.post('/edit', urlencodedParser, async function (request, response) {807router.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 */
855router.post('/edit-attribute', jsonParser, async function (request, response) {856router.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 * */
901router.post('/merge-attributes', jsonParser, async function (request, response) {902router.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});
931932
932router.post('/delete', jsonParser, async function (request, response) {933router.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});
994995
995router.post('/get', jsonParser, async function (request, response) {996router.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});
10131014
1014router.post('/chats', jsonParser, async function (request, response) {1015router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1015 if (!request.body) return response.sendStatus(400);1016 if (!request.body) return response.sendStatus(400);
10161017
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});
11621163
1163router.post('/duplicate', jsonParser, async function (request, response) {1164router.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});
12091210
1210router.post('/export', jsonParser, async function (request, response) {1211router.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);
src/endpoints/chats.js+8 -7
@@ -9,6 +9,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
9import _ from 'lodash';9import _ from 'lodash';
1010
11import { jsonParser, urlencodedParser } from '../express-common.js';11import { jsonParser, urlencodedParser } from '../express-common.js';
12import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
12import {13import {
13 getConfigValue,14 getConfigValue,
14 humanizedISO8601DateTime,15 humanizedISO8601DateTime,
@@ -294,7 +295,7 @@ function importRisuChat(userName, characterName, jsonData) {
294295
295export const router = express.Router();296export const router = express.Router();
296297
297router.post('/save', jsonParser, function (request, response) {298router.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});
312313
313router.post('/get', jsonParser, function (request, response) {314router.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});
348349
349350
350router.post('/rename', jsonParser, async function (request, response) {351router.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});
374375
375router.post('/delete', jsonParser, function (request, response) {376router.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});
390391
391router.post('/export', jsonParser, async function (request, response) {392router.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});
480481
481router.post('/import', urlencodedParser, function (request, response) {482router.post('/import', urlencodedParser, validateAvatarUrlMiddleware, function (request, response) {
482 if (!request.body) return response.sendStatus(400);483 if (!request.body) return response.sendStatus(400);
483484
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});
628629
629router.post('/search', jsonParser, function (request, response) {630router.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 = [];
src/endpoints/settings.js+3 -2
@@ -9,6 +9,7 @@ import { SETTINGS_FILE } from '../constants.js';
9import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';9import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js';
10import { jsonParser } from '../express-common.js';10import { jsonParser } from '../express-common.js';
11import { getAllUserHandles, getUserDirectories } from '../users.js';11import { getAllUserHandles, getUserDirectories } from '../users.js';
12import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1213
13const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true);14const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true);
14const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true);15const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true);
@@ -296,7 +297,7 @@ router.post('/get-snapshots', jsonParser, async (request, response) => {
296 }297 }
297});298});
298299
299router.post('/load-snapshot', jsonParser, async (request, response) => {300router.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);
302303
@@ -330,7 +331,7 @@ router.post('/make-snapshot', jsonParser, async (request, response) => {
330 }331 }
331});332});
332333
333router.post('/restore-snapshot', jsonParser, async (request, response) => {334router.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);
336337
src/middleware/validateFileName.js+34 -0
@@ -0,0 +1,34 @@
1import 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 */
8export 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
33const avatarUrlValidationFunction = getFileNameValidationFunction('avatar_url');
34export default avatarUrlValidationFunction;