Skip to content

Commit c35b1e6

Browse files
CCM-12065: Url validation
1 parent 67ef2ed commit c35b1e6

File tree

7 files changed

+152
-1
lines changed

7 files changed

+152
-1
lines changed

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,28 @@ 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: ['The message includes an insecure http:// link. All links must use https://'],
88+
},
89+
},
90+
});
91+
});
92+
7193
test('should save the template and redirect', async () => {
7294
saveTemplateMock.mockResolvedValue({
7395
...initialState,

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ 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: '**a message linking to http://www.example.com**',
81+
})
82+
);
83+
84+
expect(response).toEqual({
85+
...initialState,
86+
errorState: {
87+
formErrors: [],
88+
fieldErrors: {
89+
nhsAppTemplateMessage: ['The message includes an insecure http:// link. All links must use https://'],
90+
},
91+
},
92+
});
93+
});
94+
95+
it('create-nhs-app-template - should return response when when template message contains link with angle brackets', async () => {
96+
const response = await processFormActions(
97+
initialState,
98+
getMockFormData({
99+
'form-id': 'create-nhs-app-template',
100+
nhsAppTemplateName: 'template-name',
101+
nhsAppTemplateMessage: '**a message linking to [example.com](https://www.example.com/withparams?param1=<>)**',
102+
})
103+
);
104+
105+
expect(response).toEqual({
106+
...initialState,
107+
errorState: {
108+
formErrors: [],
109+
fieldErrors: {
110+
nhsAppTemplateMessage: ['The message includes a link that contains an angle bracket character. They must be removed or URL encoded'],
111+
},
112+
},
113+
});
114+
});
115+
74116
test('should save the template and redirect', async () => {
75117
saveTemplateMock.mockResolvedValue({
76118
...savedState,
@@ -98,6 +140,33 @@ describe('CreateNHSAppTemplate server actions', () => {
98140
);
99141
});
100142

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

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ 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: 'a message linking to http://www.example.com with http',
75+
})
76+
);
77+
78+
expect(response).toEqual({
79+
...initialState,
80+
errorState: {
81+
formErrors: [],
82+
fieldErrors: {
83+
smsTemplateMessage: ['The message includes an insecure http:// link. All links must use https://'],
84+
},
85+
},
86+
});
87+
});
88+
6889
test('should save the template and redirect', async () => {
6990
saveTemplateMock.mockResolvedValue({
7091
...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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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 = 'The message includes an insecure http:// link. All links must use https://'
1314
const selectAnOption = 'Select an option';
1415

1516
const header = {
@@ -791,6 +792,8 @@ const templateFormNhsApp = {
791792
error: {
792793
empty: enterATemplateMessage,
793794
max: templateMessageTooLong,
795+
insecureLink: templateMessageHasInsecureLink,
796+
invalidUrlCharacter: 'The message includes a link that contains an angle bracket character. They must be removed or URL encoded'
794797
},
795798
},
796799
},
@@ -886,6 +889,7 @@ const templateFormEmail = {
886889
error: {
887890
empty: enterATemplateMessage,
888891
max: templateMessageTooLong,
892+
insecureLink: templateMessageHasInsecureLink
889893
},
890894
},
891895
},
@@ -929,6 +933,7 @@ const templateFormSms = {
929933
error: {
930934
empty: enterATemplateMessage,
931935
max: templateMessageTooLong,
936+
insecureLink: templateMessageHasInsecureLink
932937
},
933938
},
934939
},

0 commit comments

Comments
 (0)