@transferwise/components 0.0.0-experimental-1d81581 → 0.0.0-experimental-a06eb0d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Markup/Markup.js +3 -2
- package/build/Markup/Markup.js.map +1 -1
- package/build/Markup/Markup.mjs +4 -3
- package/build/Markup/Markup.mjs.map +1 -1
- package/build/Markup/constants.js +0 -15
- package/build/Markup/constants.js.map +1 -1
- package/build/Markup/constants.mjs +1 -15
- package/build/Markup/constants.mjs.map +1 -1
- package/build/Markup/utils/parseMarkup.js +32 -5
- package/build/Markup/utils/parseMarkup.js.map +1 -1
- package/build/Markup/utils/parseMarkup.mjs +29 -2
- package/build/Markup/utils/parseMarkup.mjs.map +1 -1
- package/build/Markup/utils/sanitise.js +29 -28
- package/build/Markup/utils/sanitise.js.map +1 -1
- package/build/Markup/utils/sanitise.mjs +29 -28
- package/build/Markup/utils/sanitise.mjs.map +1 -1
- package/build/types/Markup/Markup.d.ts.map +1 -1
- package/build/types/Markup/constants.d.ts +0 -14
- package/build/types/Markup/constants.d.ts.map +1 -1
- package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -1
- package/build/types/Markup/utils/sanitise.d.ts +1 -1
- package/build/types/Markup/utils/sanitise.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Markup/Markup.tsx +9 -3
- package/src/Markup/constants.ts +0 -18
- package/src/Markup/utils/parseMarkup.test.ts +5 -5
- package/src/Markup/utils/parseMarkup.ts +33 -2
- package/src/Markup/utils/sanitise.test.ts +91 -89
- package/src/Markup/utils/sanitise.ts +28 -27
|
@@ -1,387 +1,389 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isHrefAllowedByMode, sanitiseHref } from './sanitise';
|
|
2
2
|
|
|
3
|
-
describe('
|
|
3
|
+
describe('sanitiseHref', () => {
|
|
4
4
|
describe('allowed protocols', () => {
|
|
5
5
|
it('allows https URLs', () => {
|
|
6
|
-
expect(
|
|
6
|
+
expect(sanitiseHref('https://example.com')).toBeDefined();
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
it('allows mailto URLs', () => {
|
|
10
|
-
expect(
|
|
10
|
+
expect(sanitiseHref('mailto:user@example.com')).toBeDefined();
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('blocks http URLs', () => {
|
|
14
|
-
expect(
|
|
14
|
+
expect(sanitiseHref('http://example.com')).toBeUndefined();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('blocks tel URLs', () => {
|
|
18
|
-
expect(
|
|
18
|
+
expect(sanitiseHref('tel:+1234567890')).toBeUndefined();
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
describe('allowed relative paths', () => {
|
|
23
23
|
it('allows root-relative paths', () => {
|
|
24
|
-
expect(
|
|
24
|
+
expect(sanitiseHref('/settings')).toBeDefined();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('allows hash fragments', () => {
|
|
28
|
-
expect(
|
|
28
|
+
expect(sanitiseHref('#section')).toBeDefined();
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
describe('blocked relative paths', () => {
|
|
33
33
|
it('blocks dot-relative paths', () => {
|
|
34
|
-
expect(
|
|
34
|
+
expect(sanitiseHref('./page')).toBeUndefined();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('blocks parent-relative paths', () => {
|
|
38
|
-
expect(
|
|
38
|
+
expect(sanitiseHref('../page')).toBeUndefined();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('blocks dot-relative paths with query parameters', () => {
|
|
42
|
-
expect(
|
|
42
|
+
expect(sanitiseHref('./page?time=10:30')).toBeUndefined();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('blocks bare relative paths with colons in segments', () => {
|
|
46
|
-
expect(
|
|
46
|
+
expect(sanitiseHref('page?key=value:other')).toBeUndefined();
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
describe('blocked protocols', () => {
|
|
51
51
|
it('blocks javascript: protocol', () => {
|
|
52
52
|
// eslint-disable-next-line no-script-url
|
|
53
|
-
expect(
|
|
53
|
+
expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('blocks javascript: with mixed case', () => {
|
|
57
57
|
// eslint-disable-next-line no-script-url
|
|
58
|
-
expect(
|
|
58
|
+
expect(sanitiseHref('JavaScript:alert(1)')).toBeUndefined();
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('blocks data: protocol', () => {
|
|
62
|
-
expect(
|
|
62
|
+
expect(sanitiseHref('data:text/html,<script>alert(1)</script>')).toBeUndefined();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it('blocks vbscript: protocol', () => {
|
|
66
|
-
expect(
|
|
66
|
+
expect(sanitiseHref('vbscript:MsgBox("xss")')).toBeUndefined();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('blocks unknown protocols', () => {
|
|
70
|
-
expect(
|
|
70
|
+
expect(sanitiseHref('ftp://evil.com')).toBeUndefined();
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
describe('protocol-relative URLs', () => {
|
|
75
75
|
it('blocks protocol-relative URLs', () => {
|
|
76
|
-
expect(
|
|
76
|
+
expect(sanitiseHref('//evil.com/phishing')).toBeUndefined();
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it('blocks protocol-relative URLs with whitespace', () => {
|
|
80
|
-
expect(
|
|
80
|
+
expect(sanitiseHref(' //evil.com ')).toBeUndefined();
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
describe('edge cases', () => {
|
|
85
85
|
it('handles empty string', () => {
|
|
86
|
-
expect(
|
|
86
|
+
expect(sanitiseHref('')).toBeUndefined();
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it('handles whitespace-padded URLs', () => {
|
|
90
|
-
expect(
|
|
90
|
+
expect(sanitiseHref(' https://example.com ')).toBe('https://example.com');
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it('handles whitespace-padded dangerous URLs', () => {
|
|
94
|
-
expect(
|
|
94
|
+
expect(sanitiseHref(' javascript:alert(1) ')).toBeUndefined();
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
describe('control characters in scheme — XSS bypass via URL parser throw', () => {
|
|
99
99
|
it('blocks null byte mid-scheme (java\\x00script:)', () => {
|
|
100
|
-
expect(
|
|
100
|
+
expect(sanitiseHref('java\x00script:alert(1)')).toBeUndefined();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
it('blocks null byte between s and c (javas\\x00cript:)', () => {
|
|
104
|
-
expect(
|
|
104
|
+
expect(sanitiseHref('javas\x00cript:alert(1)')).toBeUndefined();
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
it('blocks SOH prefix before javascript:', () => {
|
|
108
|
-
expect(
|
|
108
|
+
expect(sanitiseHref('\x01javascript:alert(1)')).toBeUndefined();
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
it('blocks vertical tab prefix before javascript:', () => {
|
|
112
|
-
expect(
|
|
112
|
+
expect(sanitiseHref('\x0Bjavascript:alert(1)')).toBeUndefined();
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('blocks form feed prefix before javascript:', () => {
|
|
116
|
-
expect(
|
|
116
|
+
expect(sanitiseHref('\x0Cjavascript:alert(1)')).toBeUndefined();
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it('blocks tab mid-scheme (java\\tscript:)', () => {
|
|
120
|
-
expect(
|
|
120
|
+
expect(sanitiseHref('java\tscript:alert(1)')).toBeUndefined();
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('blocks CRLF mid-scheme (java\\r\\nscript:)', () => {
|
|
124
|
-
expect(
|
|
124
|
+
expect(sanitiseHref('java\r\nscript:alert(1)')).toBeUndefined();
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
it('blocks LF mid-scheme (j\\navascript:)', () => {
|
|
128
|
-
expect(
|
|
128
|
+
expect(sanitiseHref('j\navascript:alert(1)')).toBeUndefined();
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
describe('zero-width and invisible character bypass', () => {
|
|
133
133
|
it('blocks zero-width space prefix before javascript:', () => {
|
|
134
|
-
expect(
|
|
134
|
+
expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
it('blocks zero-width space only (invisible link)', () => {
|
|
138
|
-
expect(
|
|
138
|
+
expect(sanitiseHref('')).toBeUndefined();
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
it('blocks BOM-only string (invisible link)', () => {
|
|
142
|
-
expect(
|
|
142
|
+
expect(sanitiseHref('')).toBeUndefined();
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
it('blocks BOM prefix before javascript:', () => {
|
|
146
|
-
expect(
|
|
146
|
+
expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
it('blocks zero-width non-joiner prefix before javascript:', () => {
|
|
150
|
-
expect(
|
|
150
|
+
expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
it('blocks zero-width joiner prefix before javascript:', () => {
|
|
154
|
-
expect(
|
|
154
|
+
expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
describe('non-ASCII whitespace prefix bypass', () => {
|
|
159
159
|
it('blocks NBSP prefix before javascript:', () => {
|
|
160
|
-
expect(
|
|
160
|
+
expect(sanitiseHref(' javascript:alert(1)')).toBeUndefined();
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
it('blocks line separator prefix before javascript:', () => {
|
|
164
|
-
expect(
|
|
164
|
+
expect(sanitiseHref('\u2028javascript:alert(1)')).toBeUndefined();
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
it('blocks paragraph separator prefix before javascript:', () => {
|
|
168
|
-
expect(
|
|
168
|
+
expect(sanitiseHref('\u2029javascript:alert(1)')).toBeUndefined();
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
it('blocks NBSP before protocol-relative URL', () => {
|
|
172
|
-
expect(
|
|
172
|
+
expect(sanitiseHref(' //evil.com')).toBeUndefined();
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
it('blocks NBSP-only string', () => {
|
|
176
|
-
expect(
|
|
176
|
+
expect(sanitiseHref(' ')).toBeUndefined();
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
describe('backslash-based open redirect', () => {
|
|
181
181
|
it('blocks /\\evil.com (browsers normalize \\ to /)', () => {
|
|
182
|
-
expect(
|
|
182
|
+
expect(sanitiseHref('/\\evil.com')).toBeUndefined();
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
it('blocks /\\\\evil.com (double backslash)', () => {
|
|
186
|
-
expect(
|
|
186
|
+
expect(sanitiseHref('/\\\\evil.com')).toBeUndefined();
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it('allows /path\\with\\backslash (not at position 1)', () => {
|
|
190
|
-
expect(
|
|
190
|
+
expect(sanitiseHref('/path\\with\\backslash')).toBeDefined();
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
describe('URL-encoded dangerous schemes', () => {
|
|
195
195
|
it('blocks javascript with URL-encoded colon (%3A)', () => {
|
|
196
|
-
expect(
|
|
196
|
+
expect(sanitiseHref('javascript%3Aalert(1)')).toBeUndefined();
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
it('blocks javascript with lowercase URL-encoded colon (%3a)', () => {
|
|
200
|
-
expect(
|
|
200
|
+
expect(sanitiseHref('javascript%3aalert(1)')).toBeUndefined();
|
|
201
201
|
});
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
describe('credential confusion / phishing via userinfo', () => {
|
|
205
205
|
it('blocks https URLs with misleading userinfo (good.com@evil.com)', () => {
|
|
206
|
-
expect(
|
|
206
|
+
expect(sanitiseHref('https://good.com@evil.com')).toBeUndefined();
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it('blocks https URLs with port-like credentials', () => {
|
|
210
|
-
expect(
|
|
210
|
+
expect(sanitiseHref('https://evil.com:443@good.com')).toBeUndefined();
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
it('allows https URLs without userinfo', () => {
|
|
214
|
-
expect(
|
|
214
|
+
expect(sanitiseHref('https://example.com/path')).toBeDefined();
|
|
215
215
|
});
|
|
216
216
|
|
|
217
217
|
it('allows mailto with @ (expected in email addresses)', () => {
|
|
218
|
-
expect(
|
|
218
|
+
expect(sanitiseHref('mailto:user@example.com')).toBeDefined();
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
222
|
describe('degenerate HTTPS URLs', () => {
|
|
223
223
|
it('blocks https:evil.com (no slashes — ambiguous host)', () => {
|
|
224
|
-
expect(
|
|
224
|
+
expect(sanitiseHref('https:evil.com')).toBeUndefined();
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it('blocks https:/evil.com (single slash — ambiguous)', () => {
|
|
228
|
-
expect(
|
|
228
|
+
expect(sanitiseHref('https:/evil.com')).toBeUndefined();
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
it('blocks https:// (no host)', () => {
|
|
232
|
-
expect(
|
|
232
|
+
expect(sanitiseHref('https://')).toBeUndefined();
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
it('blocks https:/// (empty host)', () => {
|
|
236
|
-
expect(
|
|
236
|
+
expect(sanitiseHref('https:///')).toBeUndefined();
|
|
237
237
|
});
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
describe('internal network / SSRF via allowed HTTPS', () => {
|
|
241
241
|
it('blocks https://127.0.0.1 (localhost)', () => {
|
|
242
|
-
expect(
|
|
242
|
+
expect(sanitiseHref('https://127.0.0.1/')).toBeUndefined();
|
|
243
243
|
});
|
|
244
244
|
|
|
245
245
|
it('blocks https://[::1] (IPv6 localhost)', () => {
|
|
246
|
-
expect(
|
|
246
|
+
expect(sanitiseHref('https://[::1]/')).toBeUndefined();
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
it('blocks hex-encoded IP (0x7f000001)', () => {
|
|
250
|
-
expect(
|
|
250
|
+
expect(sanitiseHref('https://0x7f000001/')).toBeUndefined();
|
|
251
251
|
});
|
|
252
252
|
|
|
253
253
|
it('blocks decimal IP (2130706433)', () => {
|
|
254
|
-
expect(
|
|
254
|
+
expect(sanitiseHref('https://2130706433/')).toBeUndefined();
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
it('blocks octal IP (0177.0.0.1)', () => {
|
|
258
|
-
expect(
|
|
258
|
+
expect(sanitiseHref('https://0177.0.0.1/')).toBeUndefined();
|
|
259
259
|
});
|
|
260
260
|
|
|
261
261
|
it('blocks AWS metadata endpoint', () => {
|
|
262
|
-
expect(
|
|
262
|
+
expect(sanitiseHref('https://169.254.169.254/')).toBeUndefined();
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
it('blocks GCP metadata endpoint', () => {
|
|
266
|
-
expect(
|
|
266
|
+
expect(sanitiseHref('https://metadata.google.internal/')).toBeUndefined();
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
it('blocks localhost with trailing dot', () => {
|
|
270
|
-
expect(
|
|
270
|
+
expect(sanitiseHref('https://localhost./')).toBeUndefined();
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
it('blocks .internal host with trailing dot', () => {
|
|
274
|
-
expect(
|
|
274
|
+
expect(sanitiseHref('https://metadata.google.internal./')).toBeUndefined();
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
it('blocks fully expanded IPv6 loopback', () => {
|
|
278
|
-
expect(
|
|
278
|
+
expect(sanitiseHref('https://[0:0:0:0:0:0:0:1]/')).toBeUndefined();
|
|
279
279
|
});
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
describe('XSS payloads in fragment and path', () => {
|
|
283
283
|
it('blocks XSS payload in hash fragment', () => {
|
|
284
|
-
expect(
|
|
284
|
+
expect(sanitiseHref('#<img onerror=alert(1)>')).toBeUndefined();
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
it('blocks XSS payload in path', () => {
|
|
288
|
-
expect(
|
|
288
|
+
expect(sanitiseHref('/<img onerror=alert(1)>')).toBeUndefined();
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
it('blocks attribute injection in hash fragment', () => {
|
|
292
|
-
expect(
|
|
292
|
+
expect(sanitiseHref('#" onclick="alert(1)')).toBeUndefined();
|
|
293
293
|
});
|
|
294
294
|
|
|
295
295
|
it('allows normal hash fragments', () => {
|
|
296
|
-
expect(
|
|
296
|
+
expect(sanitiseHref('#section-1')).toBeDefined();
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
it('allows normal root-relative paths', () => {
|
|
300
|
-
expect(
|
|
300
|
+
expect(sanitiseHref('/normal/path?query=value')).toBeDefined();
|
|
301
301
|
});
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
describe('UNC paths (Windows network shares)', () => {
|
|
305
305
|
it('blocks UNC path to external server', () => {
|
|
306
|
-
expect(
|
|
306
|
+
expect(sanitiseHref('\\\\evil.com\\share')).toBeUndefined();
|
|
307
307
|
});
|
|
308
308
|
});
|
|
309
309
|
|
|
310
310
|
describe('space before colon in scheme', () => {
|
|
311
311
|
it('blocks "javascript :alert(1)" (space before colon)', () => {
|
|
312
|
-
expect(
|
|
312
|
+
expect(sanitiseHref('javascript :alert(1)')).toBeUndefined();
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
describe('mailto abuse', () => {
|
|
317
317
|
it('blocks mailto with XSS in body parameter', () => {
|
|
318
|
-
expect(
|
|
318
|
+
expect(sanitiseHref('mailto:evil@evil.com?body=<script>alert(1)</script>')).toBeUndefined();
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
it('blocks mailto with header injection (cc/bcc)', () => {
|
|
322
|
-
expect(
|
|
322
|
+
expect(
|
|
323
|
+
sanitiseHref('mailto:evil@evil.com?cc=victim@good.com&bcc=spy@evil.com'),
|
|
324
|
+
).toBeUndefined();
|
|
323
325
|
});
|
|
324
326
|
|
|
325
327
|
it('blocks mailto with no recipient (social engineering)', () => {
|
|
326
|
-
expect(
|
|
328
|
+
expect(sanitiseHref('mailto:?subject=urgent&body=click%20here')).toBeUndefined();
|
|
327
329
|
});
|
|
328
330
|
|
|
329
331
|
it('allows simple mailto with just an address', () => {
|
|
330
|
-
expect(
|
|
332
|
+
expect(sanitiseHref('mailto:support@example.com')).toBeDefined();
|
|
331
333
|
});
|
|
332
334
|
});
|
|
333
335
|
|
|
334
336
|
describe('additional blocked protocols (regression)', () => {
|
|
335
337
|
it('blocks file: protocol', () => {
|
|
336
|
-
expect(
|
|
338
|
+
expect(sanitiseHref('file:///etc/passwd')).toBeUndefined();
|
|
337
339
|
});
|
|
338
340
|
|
|
339
341
|
it('blocks ws: protocol', () => {
|
|
340
|
-
expect(
|
|
342
|
+
expect(sanitiseHref('ws://evil.com')).toBeUndefined();
|
|
341
343
|
});
|
|
342
344
|
|
|
343
345
|
it('blocks wss: protocol', () => {
|
|
344
|
-
expect(
|
|
346
|
+
expect(sanitiseHref('wss://evil.com')).toBeUndefined();
|
|
345
347
|
});
|
|
346
348
|
|
|
347
349
|
it('blocks blob: protocol', () => {
|
|
348
|
-
expect(
|
|
350
|
+
expect(sanitiseHref('blob:https://evil.com/uuid')).toBeUndefined();
|
|
349
351
|
});
|
|
350
352
|
|
|
351
353
|
it('blocks data: with SVG XSS', () => {
|
|
352
|
-
expect(
|
|
354
|
+
expect(sanitiseHref('data:image/svg+xml,<svg onload=alert(1)>')).toBeUndefined();
|
|
353
355
|
});
|
|
354
356
|
|
|
355
357
|
it('blocks data: with base64-encoded script', () => {
|
|
356
|
-
expect(
|
|
357
|
-
|
|
358
|
-
);
|
|
358
|
+
expect(
|
|
359
|
+
sanitiseHref('data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=='),
|
|
360
|
+
).toBeUndefined();
|
|
359
361
|
});
|
|
360
362
|
|
|
361
363
|
it('blocks view-source: protocol', () => {
|
|
362
|
-
expect(
|
|
364
|
+
expect(sanitiseHref('view-source:https://evil.com')).toBeUndefined();
|
|
363
365
|
});
|
|
364
366
|
|
|
365
367
|
it('blocks custom protocol (foo:bar)', () => {
|
|
366
|
-
expect(
|
|
368
|
+
expect(sanitiseHref('foo:bar')).toBeUndefined();
|
|
367
369
|
});
|
|
368
370
|
|
|
369
371
|
it('blocks minimal protocol pattern (a:b)', () => {
|
|
370
|
-
expect(
|
|
372
|
+
expect(sanitiseHref('a:b')).toBeUndefined();
|
|
371
373
|
});
|
|
372
374
|
|
|
373
375
|
it('blocks customprotocol:payload', () => {
|
|
374
|
-
expect(
|
|
376
|
+
expect(sanitiseHref('customprotocol:payload')).toBeUndefined();
|
|
375
377
|
});
|
|
376
378
|
});
|
|
377
379
|
|
|
378
380
|
describe('whitespace-only inputs', () => {
|
|
379
381
|
it('blocks spaces only', () => {
|
|
380
|
-
expect(
|
|
382
|
+
expect(sanitiseHref(' ')).toBeUndefined();
|
|
381
383
|
});
|
|
382
384
|
|
|
383
385
|
it('blocks tab/newline/CR only', () => {
|
|
384
|
-
expect(
|
|
386
|
+
expect(sanitiseHref('\t\n\r')).toBeUndefined();
|
|
385
387
|
});
|
|
386
388
|
});
|
|
387
389
|
});
|
|
@@ -11,88 +11,88 @@ const IPV6_BRACKETS = /^\[|\]$/g;
|
|
|
11
11
|
const NUMERIC_IP = /^[\d.]+$/;
|
|
12
12
|
const HEX_IP = /^0x[0-9a-f]+$/i;
|
|
13
13
|
|
|
14
|
-
export function
|
|
14
|
+
export function sanitiseHref(href: string): string | undefined {
|
|
15
15
|
const cleaned = href.replace(INVISIBLE_CHARS, '');
|
|
16
16
|
const trimmed = cleaned.trim();
|
|
17
17
|
|
|
18
18
|
if (!trimmed) {
|
|
19
|
-
return
|
|
19
|
+
return undefined;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
if (CONTROL_CHARS.test(trimmed)) {
|
|
23
|
-
return
|
|
23
|
+
return undefined;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (DANGEROUS_HTML_CHARS.test(trimmed)) {
|
|
27
|
-
return
|
|
27
|
+
return undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
31
|
-
return
|
|
31
|
+
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (ENCODED_COLON_SCHEME.test(trimmed)) {
|
|
35
|
-
return
|
|
35
|
+
return undefined;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (trimmed.startsWith('//')) {
|
|
39
|
-
return
|
|
39
|
+
return undefined;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (trimmed.startsWith('\\')) {
|
|
43
|
-
return
|
|
43
|
+
return undefined;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (trimmed.startsWith('/')) {
|
|
47
|
-
return trimmed[1] !== '\\';
|
|
47
|
+
return trimmed[1] !== '\\' ? trimmed : undefined;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (trimmed.startsWith('#')) {
|
|
51
|
-
return
|
|
51
|
+
return trimmed;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
55
|
-
return
|
|
55
|
+
return undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
try {
|
|
59
59
|
const url = new URL(trimmed);
|
|
60
60
|
|
|
61
61
|
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
62
|
-
return
|
|
62
|
+
return undefined;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (url.protocol === 'https:') {
|
|
66
66
|
if (!trimmed.startsWith('https://')) {
|
|
67
|
-
return
|
|
67
|
+
return undefined;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (!url.hostname) {
|
|
71
|
-
return
|
|
71
|
+
return undefined;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (url.username || url.password) {
|
|
75
|
-
return
|
|
75
|
+
return undefined;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
if (isInternalHost(url.hostname)) {
|
|
79
|
-
return
|
|
79
|
+
return undefined;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (url.protocol === 'mailto:') {
|
|
84
84
|
if (url.search) {
|
|
85
|
-
return
|
|
85
|
+
return undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (!url.pathname) {
|
|
89
|
-
return
|
|
89
|
+
return undefined;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
return
|
|
93
|
+
return trimmed;
|
|
94
94
|
} catch {
|
|
95
|
-
return
|
|
95
|
+
return undefined;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -109,15 +109,16 @@ export function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean
|
|
|
109
109
|
case 'all':
|
|
110
110
|
return true;
|
|
111
111
|
case 'internal':
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return
|
|
116
|
-
} catch {
|
|
117
|
-
return false;
|
|
112
|
+
try {
|
|
113
|
+
if (!href.startsWith('/')) {
|
|
114
|
+
const parsed = new URL(href);
|
|
115
|
+
return parsed.protocol === 'https:' && parsed.hostname === 'wise.com';
|
|
118
116
|
}
|
|
117
|
+
const parsed = new URL(href, SSR_SENTINEL_ORIGIN);
|
|
118
|
+
return parsed.origin === SSR_SENTINEL_ORIGIN;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
119
121
|
}
|
|
120
|
-
return false;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|