fast-uri 4.1.3 → 4.1.4
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/index.js +27 -5
- package/lib/utils.js +8 -1
- package/package.json +1 -1
- package/test/component-safe-serialization.test.js +58 -0
- package/test/ipv6-validation.test.js +64 -0
package/index.js
CHANGED
|
@@ -328,6 +328,18 @@ function hasMalformedPercentEncoding (component) {
|
|
|
328
328
|
return false
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
|
|
333
|
+
* An unterminated `[` is not a literal, so it must still be validated as a
|
|
334
|
+
* reg-name instead of being waved through as an IP.
|
|
335
|
+
*
|
|
336
|
+
* @param {string} host
|
|
337
|
+
* @returns {boolean}
|
|
338
|
+
*/
|
|
339
|
+
function isIPLiteral (host) {
|
|
340
|
+
return host[0] === '[' && host[host.length - 1] === ']'
|
|
341
|
+
}
|
|
342
|
+
|
|
331
343
|
/**
|
|
332
344
|
* @param {RegExpMatchArray} matches
|
|
333
345
|
* @returns {boolean}
|
|
@@ -337,7 +349,7 @@ function hasMalformedComponentPercentEncoding (matches) {
|
|
|
337
349
|
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
338
350
|
const host = matches[4]
|
|
339
351
|
return hasMalformedPercentEncoding(matches[3]) ||
|
|
340
|
-
(host !== undefined && !(host
|
|
352
|
+
(host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
|
|
341
353
|
hasMalformedPercentEncoding(matches[6]) ||
|
|
342
354
|
hasMalformedPercentEncoding(matches[7]) ||
|
|
343
355
|
hasMalformedPercentEncoding(matches[8])
|
|
@@ -351,13 +363,19 @@ function hasMalformedComponentPercentEncoding (matches) {
|
|
|
351
363
|
* @returns {boolean} whether host conversion failed
|
|
352
364
|
*/
|
|
353
365
|
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
366
|
+
const bracketedIPLiteral =
|
|
367
|
+
parsed.host &&
|
|
368
|
+
parsed.host[0] === '[' &&
|
|
369
|
+
parsed.host[parsed.host.length - 1] === ']'
|
|
370
|
+
|
|
354
371
|
if (
|
|
355
372
|
!options.unicodeSupport &&
|
|
356
373
|
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
357
374
|
parsed.host &&
|
|
358
|
-
parsed.host
|
|
375
|
+
!isIPLiteral(parsed.host) &&
|
|
359
376
|
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
360
377
|
isIP === false &&
|
|
378
|
+
!bracketedIPLiteral &&
|
|
361
379
|
nonSimpleDomain(parsed.host)
|
|
362
380
|
) {
|
|
363
381
|
try {
|
|
@@ -367,6 +385,7 @@ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
|
367
385
|
return true
|
|
368
386
|
}
|
|
369
387
|
}
|
|
388
|
+
|
|
370
389
|
return false
|
|
371
390
|
}
|
|
372
391
|
|
|
@@ -480,10 +499,11 @@ function parseWithStatus (uri, opts) {
|
|
|
480
499
|
if (parsed.host) {
|
|
481
500
|
const ipv4result = isIPv4(parsed.host)
|
|
482
501
|
if (ipv4result === false) {
|
|
483
|
-
const bracketedIPLiteral = parsed.host
|
|
502
|
+
const bracketedIPLiteral = isIPLiteral(parsed.host)
|
|
503
|
+
const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1
|
|
484
504
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
485
505
|
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
486
|
-
malformedIPLiteral =
|
|
506
|
+
malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
|
|
487
507
|
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
488
508
|
|
|
489
509
|
if (malformedIPLiteral) {
|
|
@@ -513,7 +533,9 @@ function parseWithStatus (uri, opts) {
|
|
|
513
533
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
514
534
|
|
|
515
535
|
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
516
|
-
|
|
536
|
+
if (!malformedIPLiteral) {
|
|
537
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
538
|
+
}
|
|
517
539
|
|
|
518
540
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
519
541
|
if (uri.indexOf('%') !== -1) {
|
package/lib/utils.js
CHANGED
|
@@ -6,6 +6,9 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
|
|
|
6
6
|
/** @type {(value: string) => boolean} */
|
|
7
7
|
const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
|
|
8
8
|
|
|
9
|
+
/** @type {(value: string) => boolean} */
|
|
10
|
+
const isPort = RegExp.prototype.test.bind(/^\d*$/u)
|
|
11
|
+
|
|
9
12
|
/** @type {(value: string) => boolean} */
|
|
10
13
|
const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
11
14
|
|
|
@@ -769,8 +772,12 @@ function recomposeAuthority (component) {
|
|
|
769
772
|
}
|
|
770
773
|
|
|
771
774
|
if (typeof component.port === 'number' || typeof component.port === 'string') {
|
|
775
|
+
const port = String(component.port)
|
|
776
|
+
if (!isPort(port)) {
|
|
777
|
+
throw new TypeError('URI port is malformed.')
|
|
778
|
+
}
|
|
772
779
|
uriTokens.push(':')
|
|
773
|
-
uriTokens.push(
|
|
780
|
+
uriTokens.push(port)
|
|
774
781
|
}
|
|
775
782
|
|
|
776
783
|
return uriTokens.length ? uriTokens.join('') : undefined
|
package/package.json
CHANGED
|
@@ -24,6 +24,64 @@ test('userinfo serialization cannot terminate the authority', (t) => {
|
|
|
24
24
|
t.end()
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
test('port serialization rejects non-digit values', (t) => {
|
|
28
|
+
const malformedPorts = [
|
|
29
|
+
'@127.0.0.1:8124',
|
|
30
|
+
'8080@evil.example',
|
|
31
|
+
'8080/path',
|
|
32
|
+
'8080?query',
|
|
33
|
+
'8080#fragment',
|
|
34
|
+
'8080:9000',
|
|
35
|
+
'-1',
|
|
36
|
+
'1.5',
|
|
37
|
+
1.5,
|
|
38
|
+
NaN,
|
|
39
|
+
Infinity,
|
|
40
|
+
'\u0661'
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
for (const port of malformedPorts) {
|
|
44
|
+
t.throws(
|
|
45
|
+
() => fastURI.serialize({ scheme: 'http', host: 'trusted.example', port, path: '/app' }),
|
|
46
|
+
/URI port is malformed\./,
|
|
47
|
+
String(port)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
t.throws(
|
|
52
|
+
() => fastURI.normalize({ scheme: 'http', host: 'trusted.example', port: '@evil.example' }),
|
|
53
|
+
/URI port is malformed\./,
|
|
54
|
+
'object normalization rejects a malformed port'
|
|
55
|
+
)
|
|
56
|
+
t.equal(
|
|
57
|
+
fastURI.equal(
|
|
58
|
+
{ scheme: 'http', host: 'trusted.example', port: '@evil.example' },
|
|
59
|
+
{ scheme: 'http', host: 'trusted.example', port: '@evil.example' }
|
|
60
|
+
),
|
|
61
|
+
false,
|
|
62
|
+
'object equality fails closed for a malformed port'
|
|
63
|
+
)
|
|
64
|
+
t.end()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('port serialization preserves RFC 3986 digit values', (t) => {
|
|
68
|
+
const validPorts = [
|
|
69
|
+
[8080, '8080'],
|
|
70
|
+
['8080', '8080'],
|
|
71
|
+
['00080', '00080'],
|
|
72
|
+
['', '']
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
for (const [port, expected] of validPorts) {
|
|
76
|
+
t.equal(
|
|
77
|
+
fastURI.serialize({ scheme: 'uri', host: 'example.test', port }),
|
|
78
|
+
`uri://example.test:${expected}`,
|
|
79
|
+
JSON.stringify(port)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
t.end()
|
|
83
|
+
})
|
|
84
|
+
|
|
27
85
|
test('query and fragment serialization cannot inject component delimiters', (t) => {
|
|
28
86
|
const uri = fastURI.serialize({
|
|
29
87
|
scheme: 'x',
|
|
@@ -88,3 +88,67 @@ test('valid IPv6, IPvFuture, embedded IPv4, and zone forms normalize safely', (t
|
|
|
88
88
|
}
|
|
89
89
|
t.end()
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
test('IPv6 zone identifiers are validated correctly', (t) => {
|
|
93
|
+
const valid = [
|
|
94
|
+
'http://[fe80::1%25eth0]/',
|
|
95
|
+
'http://[fe80::a%en1]/',
|
|
96
|
+
'http://[fe80::a%25eth%2D0]/'
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for (const uri of valid) {
|
|
100
|
+
const parsed = fastURI.parse(uri)
|
|
101
|
+
|
|
102
|
+
t.equal(parsed.error, undefined, `${uri} parses without error`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const malformed = [
|
|
106
|
+
'http://[fe80::1%25]/',
|
|
107
|
+
'http://[fe80::1%25eth 0]/',
|
|
108
|
+
'http://[fe80::1%25eth%ZZ]/',
|
|
109
|
+
'http://[fe80::1%25K]/'
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
for (const uri of malformed) {
|
|
113
|
+
const parsed = fastURI.parse(uri)
|
|
114
|
+
|
|
115
|
+
t.equal(parsed.error, HOST_ERROR, `${uri} is rejected`)
|
|
116
|
+
t.equal(fastURI.normalize(uri), uri, `${uri} is not rewritten`)
|
|
117
|
+
t.equal(fastURI.equal(uri, uri), false, `${uri} is not comparable`)
|
|
118
|
+
}
|
|
119
|
+
t.end()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('hosts with unbalanced or misplaced IP-literal brackets are rejected', (t) => {
|
|
123
|
+
const malformed = [
|
|
124
|
+
'http://[fe80',
|
|
125
|
+
'http://[',
|
|
126
|
+
'http://[not-an-ip',
|
|
127
|
+
'http://[\u65e5\u672c',
|
|
128
|
+
'http://user@[@127.0.0.1:8123/admin',
|
|
129
|
+
'http://user@]127.0.0.1:8123/admin',
|
|
130
|
+
'http://user@prefix[@127.0.0.1:8123/admin',
|
|
131
|
+
'http://user@prefix]@127.0.0.1:8123/admin'
|
|
132
|
+
]
|
|
133
|
+
const modes = [
|
|
134
|
+
['default', undefined],
|
|
135
|
+
['Unicode', { unicodeSupport: true }]
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for (const [mode, options] of modes) {
|
|
139
|
+
for (const uri of malformed) {
|
|
140
|
+
const parsed = fastURI.parse(uri, options)
|
|
141
|
+
const message = `${mode} mode rejects ${uri}`
|
|
142
|
+
|
|
143
|
+
t.equal(parsed.error, HOST_ERROR, `parse ${message}`)
|
|
144
|
+
t.equal(fastURI.normalize(uri, options), uri, `normalize preserves ${uri} in ${mode} mode`)
|
|
145
|
+
t.equal(fastURI.equal(uri, uri, options), false, `equal ${message}`)
|
|
146
|
+
t.throws(
|
|
147
|
+
() => fastURI.resolve('http://example.com/', uri, options),
|
|
148
|
+
/URI host is malformed\./,
|
|
149
|
+
`resolve ${message}`
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
t.end()
|
|
154
|
+
})
|