Skip to content

Commit 4d10378

Browse files
CCM-12065: Url validation
1 parent 7c4b829 commit 4d10378

File tree

7 files changed

+168
-1
lines changed

7 files changed

+168
-1
lines changed

frontend/src/__tests__/components/forms/EmailTemplateForm/server-action.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ describe('CreateEmailTemplate server actions', () => {
6868
});
6969
});
7070

71+
it('create-email-template - should return response when when template message contains insecure url', async () => {
72+
const response = await processFormActions(
73+
initialState,
74+
getMockFormData({
75+
'form-id': 'create-email-template',
76+
emailTemplateName: 'template-name',
77+
emailTemplateSubjectLine: 'template-subject-line',
78+
emailTemplateMessage: '**a message linking to http://www.example.com**',
79+
})
80+
);
81+
82+
expect(response).toEqual({
83+
...initialState,
84+
errorState: {
85+
formErrors: [],
86+
fieldErrors: {
87+
emailTemplateMessage: [
88+
'The message includes an insecure http:// link. All links must use https://',
89+
],
90+
},
91+
},
92+
});
93+
});
94+
7195
test('should save the template and redirect', async () => {
7296
saveTemplateMock.mockResolvedValue({
7397
...initialState,

frontend/src/__tests__/components/forms/NhsAppTemplateForm/server-action.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,54 @@ describe('CreateNHSAppTemplate server actions', () => {
7171
});
7272
});
7373

74+
it('create-nhs-app-template - should return response when when template message contains insecure url', async () => {
75+
const response = await processFormActions(
76+
initialState,
77+
getMockFormData({
78+
'form-id': 'create-nhs-app-template',
79+
nhsAppTemplateName: 'template-name',
80+
nhsAppTemplateMessage:
81+
'**a message linking to http://www.example.com**',
82+
})
83+
);
84+
85+
expect(response).toEqual({
86+
...initialState,
87+
errorState: {
88+
formErrors: [],
89+
fieldErrors: {
90+
nhsAppTemplateMessage: [
91+
'The message includes an insecure http:// link. All links must use https://',
92+
],
93+
},
94+
},
95+
});
96+
});
97+
98+
it('create-nhs-app-template - should return response when when template message contains link with angle brackets', async () => {
99+
const response = await processFormActions(
100+
initialState,
101+
getMockFormData({
102+
'form-id': 'create-nhs-app-template',
103+
nhsAppTemplateName: 'template-name',
104+
nhsAppTemplateMessage:
105+
'**a message linking to [example.com](https://www.example.com/withparams?param1=<>)**',
106+
})
107+
);
108+
109+
expect(response).toEqual({
110+
...initialState,
111+
errorState: {
112+
formErrors: [],
113+
fieldErrors: {
114+
nhsAppTemplateMessage: [
115+
'The message includes a link that contains an angle bracket character. They must be removed or URL encoded',
116+
],
117+
},
118+
},
119+
});
120+
});
121+
74122
test('should save the template and redirect', async () => {
75123
saveTemplateMock.mockResolvedValue({
76124
...savedState,
@@ -98,6 +146,36 @@ describe('CreateNHSAppTemplate server actions', () => {
98146
);
99147
});
100148

149+
test('should save a template that contains a url with angle brackets, if they are url encoded', async () => {
150+
saveTemplateMock.mockResolvedValue({
151+
...savedState,
152+
name: 'template-name',
153+
message:
154+
'**a message linking to [example.com](https://www.example.com/withparams?param1=%3C%3E)**',
155+
});
156+
157+
await processFormActions(
158+
savedState,
159+
getMockFormData({
160+
nhsAppTemplateName: 'template-name',
161+
nhsAppTemplateMessage:
162+
'**a message linking to [example.com](https://www.example.com/withparams?param1=%3C%3E)**',
163+
})
164+
);
165+
166+
expect(saveTemplateMock).toHaveBeenCalledWith({
167+
...savedState,
168+
name: 'template-name',
169+
message:
170+
'**a message linking to [example.com](https://www.example.com/withparams?param1=%3C%3E)**',
171+
});
172+
173+
expect(redirectMock).toHaveBeenCalledWith(
174+
`/preview-nhs-app-template/template-id?from=edit`,
175+
'push'
176+
);
177+
});
178+
101179
test('should create the template and redirect', async () => {
102180
createTemplateMock.mockResolvedValue(savedState);
103181

frontend/src/__tests__/components/forms/SmsTemplateForm/server-action.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,30 @@ describe('CreateSmsTemplate server actions', () => {
6565
});
6666
});
6767

68+
it('create-sms-template - should return response when when template message contains insecure url', async () => {
69+
const response = await processFormActions(
70+
initialState,
71+
getMockFormData({
72+
'form-id': 'create-sms-template',
73+
smsTemplateName: 'template-name',
74+
smsTemplateMessage:
75+
'a message linking to http://www.example.com with http',
76+
})
77+
);
78+
79+
expect(response).toEqual({
80+
...initialState,
81+
errorState: {
82+
formErrors: [],
83+
fieldErrors: {
84+
smsTemplateMessage: [
85+
'The message includes an insecure http:// link. All links must use https://',
86+
],
87+
},
88+
},
89+
});
90+
});
91+
6892
test('should save the template and redirect', async () => {
6993
saveTemplateMock.mockResolvedValue({
7094
...initialState,

frontend/src/components/forms/EmailTemplateForm/server-action.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export const $EmailTemplateFormSchema = z.object({
2727
.min(1, { message: form.emailTemplateMessage.error.empty })
2828
.max(MAX_EMAIL_CHARACTER_LENGTH, {
2929
message: form.emailTemplateMessage.error.max,
30+
})
31+
.refine((templateMessage) => !templateMessage.includes('http://'), {
32+
message: form.emailTemplateMessage.error.insecureLink,
3033
}),
3134
});
3235

frontend/src/components/forms/NhsAppTemplateForm/server-action.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import markdownit from 'markdown-it';
12
import {
23
TemplateFormState,
34
NHSAppTemplate,
@@ -14,14 +15,41 @@ const {
1415
},
1516
} = content;
1617

18+
const hasInvalidCharactersInLinks = (message: string): boolean => {
19+
const md = markdownit();
20+
21+
const parsedMessage = md.parseInline(message, {});
22+
// [] branch should be unreachable
23+
/* istanbul ignore next */
24+
const tokens = parsedMessage[0]?.children || [];
25+
26+
for (const token of tokens) {
27+
if (token.type === 'link_open') {
28+
const href = token.attrGet('href');
29+
if (href && !message.includes(href)) {
30+
// markdown-it url-encodes angle brackets during parsing, so the original url must have included them
31+
return true;
32+
}
33+
}
34+
}
35+
return false;
36+
};
37+
1738
export const $CreateNhsAppTemplateSchema = z.object({
1839
nhsAppTemplateName: z
1940
.string({ message: form.nhsAppTemplateName.error.empty })
2041
.min(1, { message: form.nhsAppTemplateName.error.empty }),
2142
nhsAppTemplateMessage: z
2243
.string({ message: form.nhsAppTemplateMessage.error.empty })
2344
.min(1, { message: form.nhsAppTemplateMessage.error.empty })
24-
.max(5000, { message: form.nhsAppTemplateMessage.error.max }),
45+
.max(5000, { message: form.nhsAppTemplateMessage.error.max })
46+
.refine((templateMessage) => !templateMessage.includes('http://'), {
47+
message: form.nhsAppTemplateMessage.error.insecureLink,
48+
})
49+
.refine(
50+
(templateMessage) => !hasInvalidCharactersInLinks(templateMessage),
51+
{ message: form.nhsAppTemplateMessage.error.invalidUrlCharacter }
52+
),
2553
});
2654

2755
export async function processFormActions(

frontend/src/components/forms/SmsTemplateForm/server-action.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export const $CreateSmsTemplateSchema = z.object({
2424
.min(1, { message: form.smsTemplateMessage.error.empty })
2525
.max(MAX_SMS_CHARACTER_LENGTH, {
2626
message: form.smsTemplateMessage.error.max,
27+
})
28+
.refine((templateMessage) => !templateMessage.includes('http://'), {
29+
message: form.smsTemplateMessage.error.insecureLink,
2730
}),
2831
});
2932

frontend/src/content/content.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const goBackButtonText = 'Go back';
1010
const enterATemplateName = 'Enter a template name';
1111
const enterATemplateMessage = 'Enter a template message';
1212
const templateMessageTooLong = 'Template message too long';
13+
const templateMessageHasInsecureLink =
14+
'The message includes an insecure http:// link. All links must use https://';
1315
const selectAnOption = 'Select an option';
1416

1517
const header = {
@@ -791,6 +793,9 @@ const templateFormNhsApp = {
791793
error: {
792794
empty: enterATemplateMessage,
793795
max: templateMessageTooLong,
796+
insecureLink: templateMessageHasInsecureLink,
797+
invalidUrlCharacter:
798+
'The message includes a link that contains an angle bracket character. They must be removed or URL encoded',
794799
},
795800
},
796801
},
@@ -886,6 +891,7 @@ const templateFormEmail = {
886891
error: {
887892
empty: enterATemplateMessage,
888893
max: templateMessageTooLong,
894+
insecureLink: templateMessageHasInsecureLink,
889895
},
890896
},
891897
},
@@ -929,6 +935,7 @@ const templateFormSms = {
929935
error: {
930936
empty: enterATemplateMessage,
931937
max: templateMessageTooLong,
938+
insecureLink: templateMessageHasInsecureLink,
932939
},
933940
},
934941
},

0 commit comments

Comments
 (0)