From f36420004b41f8a3f07ff67c5e6c6c9319844e29 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Thu, 11 Jun 2026 14:49:55 +1200 Subject: [PATCH 1/4] Adds controller test for newsLink validation --- .../src/controllers/SettingsController.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/application/backend/src/controllers/SettingsController.test.ts b/application/backend/src/controllers/SettingsController.test.ts index 99551e33..ba556247 100644 --- a/application/backend/src/controllers/SettingsController.test.ts +++ b/application/backend/src/controllers/SettingsController.test.ts @@ -76,6 +76,17 @@ describe('SettingsController', () => { expect(response.status).toBe(422) }) + + it('should fail to update if newsLink does not match URL Regex', async () => { + const reqBody = { newsLink: 'string' } + + const response = await request(app) + .patch('/settings') + .set({ Authorization: `Bearer ${orgAdminToken}` }) + .send(reqBody) + + expect(response.status).toBe(422) + }) }) describe('GET /settings/userportal', () => { From 0618c5fdc5ed5460f62b6c8d0fe27f409afa76bd Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Fri, 12 Jun 2026 17:14:45 +1200 Subject: [PATCH 2/4] Add tcLink url validation test --- .../src/controllers/SettingsController.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/application/backend/src/controllers/SettingsController.test.ts b/application/backend/src/controllers/SettingsController.test.ts index ba556247..c21e53da 100644 --- a/application/backend/src/controllers/SettingsController.test.ts +++ b/application/backend/src/controllers/SettingsController.test.ts @@ -87,6 +87,17 @@ describe('SettingsController', () => { expect(response.status).toBe(422) }) + + it('should fail to update if tcLink does not match URL Regex', async () => { + const reqBody = { tcLink: 'string' } + + const response = await request(app) + .patch('/settings') + .set({ Authorization: `Bearer ${orgAdminToken}` }) + .send(reqBody) + + expect(response.status).toBe(422) + }) }) describe('GET /settings/userportal', () => { From 3076022edf7285340b15ad6e157bb1a52a836118 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Fri, 12 Jun 2026 17:37:39 +1200 Subject: [PATCH 3/4] Make url validation more DRY --- application/admin-client/src/pages/settings/index.tsx | 7 +++---- application/common/src/regex.ts | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/application/admin-client/src/pages/settings/index.tsx b/application/admin-client/src/pages/settings/index.tsx index 5b34b259..5198f7ab 100644 --- a/application/admin-client/src/pages/settings/index.tsx +++ b/application/admin-client/src/pages/settings/index.tsx @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom' import { axiosInstance } from '../../providers/dataProvider' import { Info } from '@mui/icons-material' import { LogoUploader } from '../../components/LogoUploader' +import { urlRegex } from '@common/src/regex' import { RESOURCES } from '../../constants' const SettingsPage = () => { @@ -88,8 +89,7 @@ const SettingsPage = () => { { Date: Fri, 12 Jun 2026 17:41:11 +1200 Subject: [PATCH 4/4] Sanitise and validate org settings URLs --- application/backend/package.json | 1 + .../src/controllers/SettingsController.ts | 29 ++++++++++++++++++- yarn.lock | 8 +++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/application/backend/package.json b/application/backend/package.json index 59138bfb..1ba9d9b3 100644 --- a/application/backend/package.json +++ b/application/backend/package.json @@ -23,6 +23,7 @@ }, "packageManager": "yarn@4.0.2", "dependencies": { + "@braintree/sanitize-url": "^7.1.2", "@prisma/client": "^5.20.0", "@tsoa/runtime": "^6.6.0", "ajv": "^8.18.0", diff --git a/application/backend/src/controllers/SettingsController.ts b/application/backend/src/controllers/SettingsController.ts index a291c553..ae1fa52f 100644 --- a/application/backend/src/controllers/SettingsController.ts +++ b/application/backend/src/controllers/SettingsController.ts @@ -18,8 +18,10 @@ import { Post, UploadedFile, Security, + ValidateError, } from 'tsoa' import { Readable } from 'stream' +import logger from 'common/src/logger' import type { GetSettingsResponse, GetUserPortalSettingsResponse, @@ -29,6 +31,17 @@ import { NotFoundErrorResponse } from 'common/types/api/errors' import { NotFoundError } from '../middlewares/ErrorHandler' import { auditLog } from '../middlewares/AuditLog' import { processLogoImage } from 'common/src/imageHelpers' +import { urlRegex } from 'common/src/regex' +import { sanitizeUrl } from '@braintree/sanitize-url' + +function sanitiseAndValidateUrl(link: string) { + const validatedUrl = sanitizeUrl(link) + if (urlRegex.test(validatedUrl)) { + return validatedUrl + } else { + throw new Error('urlRegex failed') + } +} @Route('settings') @Tags('Settings') @@ -71,7 +84,21 @@ export class SettingsController extends Controller { @Security('jwt', ['OrganisationAdmin']) @Response('422', 'Validation Failed') public async updateSettings(@Body() bodyRequest: UpdateSettingsRequest) { - await prisma.organisation.update({ where: { id: 1 }, data: bodyRequest }) + const data = bodyRequest + // There is already validation on Admin Portal UI but best to not store unsanitized data + Object.keys(data) + .filter((urlKey) => urlKey === 'newsLink' || urlKey === 'tcLink') + .forEach((key) => { + try { + data[key] = sanitiseAndValidateUrl(data[key] as string) + } catch (err) { + const errorMessage: string = `Link is not acceptable URL: ${data.newsLink}` + logger.error({ errorMessage, err }) + throw new ValidateError({}, errorMessage) + } + }) + + await prisma.organisation.update({ where: { id: 1 }, data: data }) } @Get('/userportal') diff --git a/yarn.lock b/yarn.lock index a7c8eb3e..5e5431d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -812,6 +812,13 @@ __metadata: languageName: node linkType: hard +"@braintree/sanitize-url@npm:^7.1.2": + version: 7.1.2 + resolution: "@braintree/sanitize-url@npm:7.1.2" + checksum: 10c0/62f2aa0cf58626e3880b2dc1025c42064b4639abd157ae4e1c35f4c2f5031e9273772046a423979845069c814e27ff818e8e669280dc53585e6f033d5b7a59cb + languageName: node + linkType: hard + "@chakra-ui/anatomy@npm:2.2.2": version: 2.2.2 resolution: "@chakra-ui/anatomy@npm:2.2.2" @@ -6517,6 +6524,7 @@ __metadata: version: 0.0.0-use.local resolution: "backend@workspace:application/backend" dependencies: + "@braintree/sanitize-url": "npm:^7.1.2" "@jest/globals": "npm:^29.7.0" "@prisma/client": "npm:^5.20.0" "@tsoa/runtime": "npm:^6.6.0"