From c005816748dcd5dd37cdfe6e21f6315a0d6ec2e2 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Mon, 13 Oct 2025 12:45:08 +0200 Subject: [PATCH] fix(api): handle invalid picture URLs for '/update-my-about' (#61769) --- api/src/routes/protected/settings.test.ts | 63 ++++++++++++++++++++++- api/src/routes/protected/settings.ts | 48 ++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/api/src/routes/protected/settings.test.ts b/api/src/routes/protected/settings.test.ts index 466f299d57f..66df23ca5d7 100644 --- a/api/src/routes/protected/settings.test.ts +++ b/api/src/routes/protected/settings.test.ts @@ -871,15 +871,74 @@ Happy coding! expect(response.statusCode).toEqual(200); }); - test('PUT updates the values in about settings without image', async () => { + test('PUT returns 400 if the URL is invalid', async () => { const response = await superPut('/update-my-about').send({ about: 'Teacher at freeCodeCamp', name: 'Quincy Larson', location: 'USA', - // `new URL` throws if the image isn't a URL, this checks if it doesn't throw. picture: 'invalid' }); + expect(response.body).toEqual({ + message: 'flash.wrong-updating', + type: 'danger' + }); + expect(response.statusCode).toEqual(400); + }); + + test('PUT returns 400 if the URL has no image extension', async () => { + const response = await superPut('/update-my-about').send({ + about: 'Teacher at freeCodeCamp', + name: 'Quincy Larson', + location: 'USA', + picture: 'https://example.com/avatar' + }); + + expect(response.body).toEqual({ + message: 'flash.wrong-updating', + type: 'danger' + }); + expect(response.statusCode).toEqual(400); + }); + + test('PUT returns 400 if the URL has a non-image extension', async () => { + const response = await superPut('/update-my-about').send({ + about: 'Teacher at freeCodeCamp', + name: 'Quincy Larson', + location: 'USA', + picture: 'https://example.com/file.txt' + }); + + expect(response.body).toEqual({ + message: 'flash.wrong-updating', + type: 'danger' + }); + expect(response.statusCode).toEqual(400); + }); + + test('PUT accepts an image URL with query string', async () => { + const response = await superPut('/update-my-about').send({ + about: 'Teacher at freeCodeCamp', + name: 'Quincy Larson', + location: 'USA', + picture: 'https://example.com/photo.png?size=200&cache=bust' + }); + + expect(response.body).toEqual({ + message: 'flash.updated-about-me', + type: 'success' + }); + expect(response.statusCode).toEqual(200); + }); + + test('PUT accepts an image URL with a different valid extension (.webp)', async () => { + const response = await superPut('/update-my-about').send({ + about: 'Teacher at freeCodeCamp', + name: 'Quincy Larson', + location: 'USA', + picture: 'https://example.com/avatar.webp' + }); + expect(response.body).toEqual({ message: 'flash.updated-about-me', type: 'success' diff --git a/api/src/routes/protected/settings.ts b/api/src/routes/protected/settings.ts index 8090e93978d..b94d39c2b85 100644 --- a/api/src/routes/protected/settings.ts +++ b/api/src/routes/protected/settings.ts @@ -53,6 +53,45 @@ export const isPictureWithProtocol = (picture?: string): boolean => { } }; +const commonImageExtensions = [ + 'apng', + 'avif', + 'gif', + 'jpg', + 'jpeg', + 'jfif', + 'pjpeg', + 'pjp', + 'png', + 'svg', + 'webp' +]; + +/** + * Validate that a picture URL has a common image extension. + * + * @param picture The URL to check. + * @returns Whether the URL has a common image extension. + */ + +const validateImageExtension = (picture?: string): boolean => { + if (!picture) return true; + return commonImageExtensions.some(ext => picture.includes(`.${ext}`)); +}; + +/** + * Validate that a picture URL is valid. A valid picture URL either: + * - is empty/undefined (no update), or + * - has a valid http/https protocol AND has a common image extension. + * + * @param picture The URL to validate. + * @returns Whether the picture URL is considered valid. + */ +const isValidPictureUrl = (picture?: string): boolean => { + if (!picture) return true; + return isPictureWithProtocol(picture) && validateImageExtension(picture); +}; + const ALLOWED_DOMAINS_MAP = { githubProfile: ['github.com'], linkedin: ['linkedin.com'], @@ -484,7 +523,12 @@ ${isLinkSentWithinLimitTTL}` }, async (req, reply) => { const logger = fastify.log.child({ req, res: reply }); - const hasProtocol = isPictureWithProtocol(req.body.picture); + const pictureIsValid = isValidPictureUrl(req.body.picture); + if (!pictureIsValid) { + logger.warn(`Invalid picture URL: ${req.body.picture}`); + void reply.code(400); + return { message: 'flash.wrong-updating', type: 'danger' } as const; + } try { await fastify.prisma.user.update({ @@ -493,7 +537,7 @@ ${isLinkSentWithinLimitTTL}` about: req.body.about, name: req.body.name, location: req.body.location, - picture: hasProtocol ? req.body.picture : '' + picture: req.body.picture } });