fast-uri 2.4.5 → 2.4.6
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 +20 -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 +40 -0
package/index.js
CHANGED
|
@@ -283,7 +283,7 @@ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
|
283
283
|
!options.unicodeSupport &&
|
|
284
284
|
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
285
285
|
parsed.host &&
|
|
286
|
-
parsed.host
|
|
286
|
+
!isIPLiteral(parsed.host) &&
|
|
287
287
|
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
288
288
|
isIP === false &&
|
|
289
289
|
nonSimpleDomain(parsed.host)
|
|
@@ -312,6 +312,18 @@ function hasMalformedPercentEncoding (component) {
|
|
|
312
312
|
return false
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
|
|
317
|
+
* An unterminated `[` is not a literal, so it must still be validated as a
|
|
318
|
+
* reg-name instead of being waved through as an IP.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} host
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
function isIPLiteral (host) {
|
|
324
|
+
return host[0] === '[' && host[host.length - 1] === ']'
|
|
325
|
+
}
|
|
326
|
+
|
|
315
327
|
/**
|
|
316
328
|
* @param {RegExpMatchArray} matches
|
|
317
329
|
* @returns {boolean}
|
|
@@ -321,7 +333,7 @@ function hasMalformedComponentPercentEncoding (matches) {
|
|
|
321
333
|
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
322
334
|
const host = matches[4]
|
|
323
335
|
return hasMalformedPercentEncoding(matches[3]) ||
|
|
324
|
-
(host !== undefined && !(host
|
|
336
|
+
(host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
|
|
325
337
|
hasMalformedPercentEncoding(matches[6]) ||
|
|
326
338
|
hasMalformedPercentEncoding(matches[7]) ||
|
|
327
339
|
hasMalformedPercentEncoding(matches[8])
|
|
@@ -423,10 +435,11 @@ function parseWithStatus (uri, opts) {
|
|
|
423
435
|
if (parsed.host) {
|
|
424
436
|
const ipv4result = isIPv4(parsed.host)
|
|
425
437
|
if (ipv4result === false) {
|
|
426
|
-
const bracketedIPLiteral = parsed.host
|
|
438
|
+
const bracketedIPLiteral = isIPLiteral(parsed.host)
|
|
439
|
+
const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1
|
|
427
440
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
428
441
|
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
429
|
-
malformedIPLiteral =
|
|
442
|
+
malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
|
|
430
443
|
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
431
444
|
|
|
432
445
|
if (malformedIPLiteral) {
|
|
@@ -456,7 +469,9 @@ function parseWithStatus (uri, opts) {
|
|
|
456
469
|
const schemeHandler = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()]
|
|
457
470
|
|
|
458
471
|
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
459
|
-
|
|
472
|
+
if (!malformedIPLiteral) {
|
|
473
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
474
|
+
}
|
|
460
475
|
|
|
461
476
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
462
477
|
if (parsed.host !== undefined && !malformedIPLiteral) {
|
package/lib/utils.js
CHANGED
|
@@ -5,6 +5,9 @@ const { HEX } = require('./scopedChars')
|
|
|
5
5
|
/** @type {(value: string) => boolean} */
|
|
6
6
|
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)
|
|
7
7
|
|
|
8
|
+
/** @type {(value: string) => boolean} */
|
|
9
|
+
const isPort = RegExp.prototype.test.bind(/^\d*$/u)
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* @param {string[]} input
|
|
10
13
|
* @param {boolean} [keepZero=false]
|
|
@@ -630,8 +633,12 @@ function recomposeAuthority (components, options) {
|
|
|
630
633
|
}
|
|
631
634
|
|
|
632
635
|
if (typeof components.port === 'number' || typeof components.port === 'string') {
|
|
636
|
+
const port = String(components.port)
|
|
637
|
+
if (!isPort(port)) {
|
|
638
|
+
throw new TypeError('URI port is malformed.')
|
|
639
|
+
}
|
|
633
640
|
uriTokens.push(':')
|
|
634
|
-
uriTokens.push(
|
|
641
|
+
uriTokens.push(port)
|
|
635
642
|
}
|
|
636
643
|
|
|
637
644
|
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',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tape')
|
|
4
|
+
const fastURI = require('..')
|
|
5
|
+
|
|
6
|
+
const HOST_ERROR = 'URI host is malformed.'
|
|
7
|
+
|
|
8
|
+
test('hosts with unbalanced or misplaced IP-literal brackets are rejected', (t) => {
|
|
9
|
+
const malformed = [
|
|
10
|
+
'http://[fe80',
|
|
11
|
+
'http://[',
|
|
12
|
+
'http://[not-an-ip',
|
|
13
|
+
'http://[\u65e5\u672c',
|
|
14
|
+
'http://user@[@127.0.0.1:8123/admin',
|
|
15
|
+
'http://user@]127.0.0.1:8123/admin',
|
|
16
|
+
'http://user@prefix[@127.0.0.1:8123/admin',
|
|
17
|
+
'http://user@prefix]@127.0.0.1:8123/admin'
|
|
18
|
+
]
|
|
19
|
+
const modes = [
|
|
20
|
+
['default', undefined],
|
|
21
|
+
['Unicode', { unicodeSupport: true }]
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for (const [mode, options] of modes) {
|
|
25
|
+
for (const uri of malformed) {
|
|
26
|
+
const parsed = fastURI.parse(uri, options)
|
|
27
|
+
const message = `${mode} mode rejects ${uri}`
|
|
28
|
+
|
|
29
|
+
t.equal(parsed.error, HOST_ERROR, `parse ${message}`)
|
|
30
|
+
t.equal(fastURI.normalize(uri, options), uri, `normalize preserves ${uri} in ${mode} mode`)
|
|
31
|
+
t.equal(fastURI.equal(uri, uri, options), false, `equal ${message}`)
|
|
32
|
+
t.throws(
|
|
33
|
+
() => fastURI.resolve('http://example.com/', uri, options),
|
|
34
|
+
/URI host is malformed\./,
|
|
35
|
+
`resolve ${message}`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
t.end()
|
|
40
|
+
})
|