@transferwise/components 0.0.0-experimental-3586f3f → 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.
@@ -1,387 +1,389 @@
1
- import { isAllowedHref, isHrefAllowedByMode } from './sanitise';
1
+ import { isHrefAllowedByMode, sanitiseHref } from './sanitise';
2
2
 
3
- describe('isAllowedHref', () => {
3
+ describe('sanitiseHref', () => {
4
4
  describe('allowed protocols', () => {
5
5
  it('allows https URLs', () => {
6
- expect(isAllowedHref('https://example.com')).toBe(true);
6
+ expect(sanitiseHref('https://example.com')).toBeDefined();
7
7
  });
8
8
 
9
9
  it('allows mailto URLs', () => {
10
- expect(isAllowedHref('mailto:user@example.com')).toBe(true);
10
+ expect(sanitiseHref('mailto:user@example.com')).toBeDefined();
11
11
  });
12
12
 
13
13
  it('blocks http URLs', () => {
14
- expect(isAllowedHref('http://example.com')).toBe(false);
14
+ expect(sanitiseHref('http://example.com')).toBeUndefined();
15
15
  });
16
16
 
17
17
  it('blocks tel URLs', () => {
18
- expect(isAllowedHref('tel:+1234567890')).toBe(false);
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(isAllowedHref('/settings')).toBe(true);
24
+ expect(sanitiseHref('/settings')).toBeDefined();
25
25
  });
26
26
 
27
27
  it('allows hash fragments', () => {
28
- expect(isAllowedHref('#section')).toBe(true);
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(isAllowedHref('./page')).toBe(false);
34
+ expect(sanitiseHref('./page')).toBeUndefined();
35
35
  });
36
36
 
37
37
  it('blocks parent-relative paths', () => {
38
- expect(isAllowedHref('../page')).toBe(false);
38
+ expect(sanitiseHref('../page')).toBeUndefined();
39
39
  });
40
40
 
41
41
  it('blocks dot-relative paths with query parameters', () => {
42
- expect(isAllowedHref('./page?time=10:30')).toBe(false);
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(isAllowedHref('page?key=value:other')).toBe(false);
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(isAllowedHref('javascript:alert(1)')).toBe(false);
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(isAllowedHref('JavaScript:alert(1)')).toBe(false);
58
+ expect(sanitiseHref('JavaScript:alert(1)')).toBeUndefined();
59
59
  });
60
60
 
61
61
  it('blocks data: protocol', () => {
62
- expect(isAllowedHref('data:text/html,<script>alert(1)</script>')).toBe(false);
62
+ expect(sanitiseHref('data:text/html,<script>alert(1)</script>')).toBeUndefined();
63
63
  });
64
64
 
65
65
  it('blocks vbscript: protocol', () => {
66
- expect(isAllowedHref('vbscript:MsgBox("xss")')).toBe(false);
66
+ expect(sanitiseHref('vbscript:MsgBox("xss")')).toBeUndefined();
67
67
  });
68
68
 
69
69
  it('blocks unknown protocols', () => {
70
- expect(isAllowedHref('ftp://evil.com')).toBe(false);
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(isAllowedHref('//evil.com/phishing')).toBe(false);
76
+ expect(sanitiseHref('//evil.com/phishing')).toBeUndefined();
77
77
  });
78
78
 
79
79
  it('blocks protocol-relative URLs with whitespace', () => {
80
- expect(isAllowedHref(' //evil.com ')).toBe(false);
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(isAllowedHref('')).toBe(false);
86
+ expect(sanitiseHref('')).toBeUndefined();
87
87
  });
88
88
 
89
89
  it('handles whitespace-padded URLs', () => {
90
- expect(isAllowedHref(' https://example.com ')).toBe(true);
90
+ expect(sanitiseHref(' https://example.com ')).toBe('https://example.com');
91
91
  });
92
92
 
93
93
  it('handles whitespace-padded dangerous URLs', () => {
94
- expect(isAllowedHref(' javascript:alert(1) ')).toBe(false);
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(isAllowedHref('java\x00script:alert(1)')).toBe(false);
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(isAllowedHref('javas\x00cript:alert(1)')).toBe(false);
104
+ expect(sanitiseHref('javas\x00cript:alert(1)')).toBeUndefined();
105
105
  });
106
106
 
107
107
  it('blocks SOH prefix before javascript:', () => {
108
- expect(isAllowedHref('\x01javascript:alert(1)')).toBe(false);
108
+ expect(sanitiseHref('\x01javascript:alert(1)')).toBeUndefined();
109
109
  });
110
110
 
111
111
  it('blocks vertical tab prefix before javascript:', () => {
112
- expect(isAllowedHref('\x0Bjavascript:alert(1)')).toBe(false);
112
+ expect(sanitiseHref('\x0Bjavascript:alert(1)')).toBeUndefined();
113
113
  });
114
114
 
115
115
  it('blocks form feed prefix before javascript:', () => {
116
- expect(isAllowedHref('\x0Cjavascript:alert(1)')).toBe(false);
116
+ expect(sanitiseHref('\x0Cjavascript:alert(1)')).toBeUndefined();
117
117
  });
118
118
 
119
119
  it('blocks tab mid-scheme (java\\tscript:)', () => {
120
- expect(isAllowedHref('java\tscript:alert(1)')).toBe(false);
120
+ expect(sanitiseHref('java\tscript:alert(1)')).toBeUndefined();
121
121
  });
122
122
 
123
123
  it('blocks CRLF mid-scheme (java\\r\\nscript:)', () => {
124
- expect(isAllowedHref('java\r\nscript:alert(1)')).toBe(false);
124
+ expect(sanitiseHref('java\r\nscript:alert(1)')).toBeUndefined();
125
125
  });
126
126
 
127
127
  it('blocks LF mid-scheme (j\\navascript:)', () => {
128
- expect(isAllowedHref('j\navascript:alert(1)')).toBe(false);
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(isAllowedHref('​javascript:alert(1)')).toBe(false);
134
+ expect(sanitiseHref('​javascript:alert(1)')).toBeUndefined();
135
135
  });
136
136
 
137
137
  it('blocks zero-width space only (invisible link)', () => {
138
- expect(isAllowedHref('​')).toBe(false);
138
+ expect(sanitiseHref('​')).toBeUndefined();
139
139
  });
140
140
 
141
141
  it('blocks BOM-only string (invisible link)', () => {
142
- expect(isAllowedHref('')).toBe(false);
142
+ expect(sanitiseHref('')).toBeUndefined();
143
143
  });
144
144
 
145
145
  it('blocks BOM prefix before javascript:', () => {
146
- expect(isAllowedHref('javascript:alert(1)')).toBe(false);
146
+ expect(sanitiseHref('javascript:alert(1)')).toBeUndefined();
147
147
  });
148
148
 
149
149
  it('blocks zero-width non-joiner prefix before javascript:', () => {
150
- expect(isAllowedHref('‌javascript:alert(1)')).toBe(false);
150
+ expect(sanitiseHref('‌javascript:alert(1)')).toBeUndefined();
151
151
  });
152
152
 
153
153
  it('blocks zero-width joiner prefix before javascript:', () => {
154
- expect(isAllowedHref('‍javascript:alert(1)')).toBe(false);
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(isAllowedHref(' javascript:alert(1)')).toBe(false);
160
+ expect(sanitiseHref(' javascript:alert(1)')).toBeUndefined();
161
161
  });
162
162
 
163
163
  it('blocks line separator prefix before javascript:', () => {
164
- expect(isAllowedHref('\u2028javascript:alert(1)')).toBe(false);
164
+ expect(sanitiseHref('\u2028javascript:alert(1)')).toBeUndefined();
165
165
  });
166
166
 
167
167
  it('blocks paragraph separator prefix before javascript:', () => {
168
- expect(isAllowedHref('\u2029javascript:alert(1)')).toBe(false);
168
+ expect(sanitiseHref('\u2029javascript:alert(1)')).toBeUndefined();
169
169
  });
170
170
 
171
171
  it('blocks NBSP before protocol-relative URL', () => {
172
- expect(isAllowedHref(' //evil.com')).toBe(false);
172
+ expect(sanitiseHref(' //evil.com')).toBeUndefined();
173
173
  });
174
174
 
175
175
  it('blocks NBSP-only string', () => {
176
- expect(isAllowedHref(' ')).toBe(false);
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(isAllowedHref('/\\evil.com')).toBe(false);
182
+ expect(sanitiseHref('/\\evil.com')).toBeUndefined();
183
183
  });
184
184
 
185
185
  it('blocks /\\\\evil.com (double backslash)', () => {
186
- expect(isAllowedHref('/\\\\evil.com')).toBe(false);
186
+ expect(sanitiseHref('/\\\\evil.com')).toBeUndefined();
187
187
  });
188
188
 
189
189
  it('allows /path\\with\\backslash (not at position 1)', () => {
190
- expect(isAllowedHref('/path\\with\\backslash')).toBe(true);
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(isAllowedHref('javascript%3Aalert(1)')).toBe(false);
196
+ expect(sanitiseHref('javascript%3Aalert(1)')).toBeUndefined();
197
197
  });
198
198
 
199
199
  it('blocks javascript with lowercase URL-encoded colon (%3a)', () => {
200
- expect(isAllowedHref('javascript%3aalert(1)')).toBe(false);
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(isAllowedHref('https://good.com@evil.com')).toBe(false);
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(isAllowedHref('https://evil.com:443@good.com')).toBe(false);
210
+ expect(sanitiseHref('https://evil.com:443@good.com')).toBeUndefined();
211
211
  });
212
212
 
213
213
  it('allows https URLs without userinfo', () => {
214
- expect(isAllowedHref('https://example.com/path')).toBe(true);
214
+ expect(sanitiseHref('https://example.com/path')).toBeDefined();
215
215
  });
216
216
 
217
217
  it('allows mailto with @ (expected in email addresses)', () => {
218
- expect(isAllowedHref('mailto:user@example.com')).toBe(true);
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(isAllowedHref('https:evil.com')).toBe(false);
224
+ expect(sanitiseHref('https:evil.com')).toBeUndefined();
225
225
  });
226
226
 
227
227
  it('blocks https:/evil.com (single slash — ambiguous)', () => {
228
- expect(isAllowedHref('https:/evil.com')).toBe(false);
228
+ expect(sanitiseHref('https:/evil.com')).toBeUndefined();
229
229
  });
230
230
 
231
231
  it('blocks https:// (no host)', () => {
232
- expect(isAllowedHref('https://')).toBe(false);
232
+ expect(sanitiseHref('https://')).toBeUndefined();
233
233
  });
234
234
 
235
235
  it('blocks https:/// (empty host)', () => {
236
- expect(isAllowedHref('https:///')).toBe(false);
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(isAllowedHref('https://127.0.0.1/')).toBe(false);
242
+ expect(sanitiseHref('https://127.0.0.1/')).toBeUndefined();
243
243
  });
244
244
 
245
245
  it('blocks https://[::1] (IPv6 localhost)', () => {
246
- expect(isAllowedHref('https://[::1]/')).toBe(false);
246
+ expect(sanitiseHref('https://[::1]/')).toBeUndefined();
247
247
  });
248
248
 
249
249
  it('blocks hex-encoded IP (0x7f000001)', () => {
250
- expect(isAllowedHref('https://0x7f000001/')).toBe(false);
250
+ expect(sanitiseHref('https://0x7f000001/')).toBeUndefined();
251
251
  });
252
252
 
253
253
  it('blocks decimal IP (2130706433)', () => {
254
- expect(isAllowedHref('https://2130706433/')).toBe(false);
254
+ expect(sanitiseHref('https://2130706433/')).toBeUndefined();
255
255
  });
256
256
 
257
257
  it('blocks octal IP (0177.0.0.1)', () => {
258
- expect(isAllowedHref('https://0177.0.0.1/')).toBe(false);
258
+ expect(sanitiseHref('https://0177.0.0.1/')).toBeUndefined();
259
259
  });
260
260
 
261
261
  it('blocks AWS metadata endpoint', () => {
262
- expect(isAllowedHref('https://169.254.169.254/')).toBe(false);
262
+ expect(sanitiseHref('https://169.254.169.254/')).toBeUndefined();
263
263
  });
264
264
 
265
265
  it('blocks GCP metadata endpoint', () => {
266
- expect(isAllowedHref('https://metadata.google.internal/')).toBe(false);
266
+ expect(sanitiseHref('https://metadata.google.internal/')).toBeUndefined();
267
267
  });
268
268
 
269
269
  it('blocks localhost with trailing dot', () => {
270
- expect(isAllowedHref('https://localhost./')).toBe(false);
270
+ expect(sanitiseHref('https://localhost./')).toBeUndefined();
271
271
  });
272
272
 
273
273
  it('blocks .internal host with trailing dot', () => {
274
- expect(isAllowedHref('https://metadata.google.internal./')).toBe(false);
274
+ expect(sanitiseHref('https://metadata.google.internal./')).toBeUndefined();
275
275
  });
276
276
 
277
277
  it('blocks fully expanded IPv6 loopback', () => {
278
- expect(isAllowedHref('https://[0:0:0:0:0:0:0:1]/')).toBe(false);
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(isAllowedHref('#<img onerror=alert(1)>')).toBe(false);
284
+ expect(sanitiseHref('#<img onerror=alert(1)>')).toBeUndefined();
285
285
  });
286
286
 
287
287
  it('blocks XSS payload in path', () => {
288
- expect(isAllowedHref('/<img onerror=alert(1)>')).toBe(false);
288
+ expect(sanitiseHref('/<img onerror=alert(1)>')).toBeUndefined();
289
289
  });
290
290
 
291
291
  it('blocks attribute injection in hash fragment', () => {
292
- expect(isAllowedHref('#" onclick="alert(1)')).toBe(false);
292
+ expect(sanitiseHref('#" onclick="alert(1)')).toBeUndefined();
293
293
  });
294
294
 
295
295
  it('allows normal hash fragments', () => {
296
- expect(isAllowedHref('#section-1')).toBe(true);
296
+ expect(sanitiseHref('#section-1')).toBeDefined();
297
297
  });
298
298
 
299
299
  it('allows normal root-relative paths', () => {
300
- expect(isAllowedHref('/normal/path?query=value')).toBe(true);
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(isAllowedHref('\\\\evil.com\\share')).toBe(false);
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(isAllowedHref('javascript :alert(1)')).toBe(false);
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(isAllowedHref('mailto:evil@evil.com?body=<script>alert(1)</script>')).toBe(false);
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(isAllowedHref('mailto:evil@evil.com?cc=victim@good.com&bcc=spy@evil.com')).toBe(false);
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(isAllowedHref('mailto:?subject=urgent&body=click%20here')).toBe(false);
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(isAllowedHref('mailto:support@example.com')).toBe(true);
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(isAllowedHref('file:///etc/passwd')).toBe(false);
338
+ expect(sanitiseHref('file:///etc/passwd')).toBeUndefined();
337
339
  });
338
340
 
339
341
  it('blocks ws: protocol', () => {
340
- expect(isAllowedHref('ws://evil.com')).toBe(false);
342
+ expect(sanitiseHref('ws://evil.com')).toBeUndefined();
341
343
  });
342
344
 
343
345
  it('blocks wss: protocol', () => {
344
- expect(isAllowedHref('wss://evil.com')).toBe(false);
346
+ expect(sanitiseHref('wss://evil.com')).toBeUndefined();
345
347
  });
346
348
 
347
349
  it('blocks blob: protocol', () => {
348
- expect(isAllowedHref('blob:https://evil.com/uuid')).toBe(false);
350
+ expect(sanitiseHref('blob:https://evil.com/uuid')).toBeUndefined();
349
351
  });
350
352
 
351
353
  it('blocks data: with SVG XSS', () => {
352
- expect(isAllowedHref('data:image/svg+xml,<svg onload=alert(1)>')).toBe(false);
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(isAllowedHref('data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==')).toBe(
357
- false,
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(isAllowedHref('view-source:https://evil.com')).toBe(false);
364
+ expect(sanitiseHref('view-source:https://evil.com')).toBeUndefined();
363
365
  });
364
366
 
365
367
  it('blocks custom protocol (foo:bar)', () => {
366
- expect(isAllowedHref('foo:bar')).toBe(false);
368
+ expect(sanitiseHref('foo:bar')).toBeUndefined();
367
369
  });
368
370
 
369
371
  it('blocks minimal protocol pattern (a:b)', () => {
370
- expect(isAllowedHref('a:b')).toBe(false);
372
+ expect(sanitiseHref('a:b')).toBeUndefined();
371
373
  });
372
374
 
373
375
  it('blocks customprotocol:payload', () => {
374
- expect(isAllowedHref('customprotocol:payload')).toBe(false);
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(isAllowedHref(' ')).toBe(false);
382
+ expect(sanitiseHref(' ')).toBeUndefined();
381
383
  });
382
384
 
383
385
  it('blocks tab/newline/CR only', () => {
384
- expect(isAllowedHref('\t\n\r')).toBe(false);
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 isAllowedHref(href: string): boolean {
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 false;
19
+ return undefined;
20
20
  }
21
21
 
22
22
  if (CONTROL_CHARS.test(trimmed)) {
23
- return false;
23
+ return undefined;
24
24
  }
25
25
 
26
26
  if (DANGEROUS_HTML_CHARS.test(trimmed)) {
27
- return false;
27
+ return undefined;
28
28
  }
29
29
 
30
30
  if (DANGEROUS_SCHEME.test(trimmed)) {
31
- return false;
31
+ return undefined;
32
32
  }
33
33
 
34
34
  if (ENCODED_COLON_SCHEME.test(trimmed)) {
35
- return false;
35
+ return undefined;
36
36
  }
37
37
 
38
38
  if (trimmed.startsWith('//')) {
39
- return false;
39
+ return undefined;
40
40
  }
41
41
 
42
42
  if (trimmed.startsWith('\\')) {
43
- return false;
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 true;
51
+ return trimmed;
52
52
  }
53
53
 
54
54
  if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
55
- return false;
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 false;
62
+ return undefined;
63
63
  }
64
64
 
65
65
  if (url.protocol === 'https:') {
66
66
  if (!trimmed.startsWith('https://')) {
67
- return false;
67
+ return undefined;
68
68
  }
69
69
 
70
70
  if (!url.hostname) {
71
- return false;
71
+ return undefined;
72
72
  }
73
73
 
74
74
  if (url.username || url.password) {
75
- return false;
75
+ return undefined;
76
76
  }
77
77
 
78
78
  if (isInternalHost(url.hostname)) {
79
- return false;
79
+ return undefined;
80
80
  }
81
81
  }
82
82
 
83
83
  if (url.protocol === 'mailto:') {
84
84
  if (url.search) {
85
- return false;
85
+ return undefined;
86
86
  }
87
87
 
88
88
  if (!url.pathname) {
89
- return false;
89
+ return undefined;
90
90
  }
91
91
  }
92
92
 
93
- return true;
93
+ return trimmed;
94
94
  } catch {
95
- return false;
95
+ return undefined;
96
96
  }
97
97
  }
98
98