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 = () => { { { 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) + }) + + 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', () => { 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/application/common/src/regex.ts b/application/common/src/regex.ts index 6482b916..2c349b68 100644 --- a/application/common/src/regex.ts +++ b/application/common/src/regex.ts @@ -1 +1,4 @@ export const emailRegex = /^[\w.-]+@([\w-]+\.)+[\w-]{2,63}$/ + +export const urlRegex = + /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/ 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"