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 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[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(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[0] !== '[' &&
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[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
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 = bracketedIPLiteral && ipv6result.error === true
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
- malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
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(String(component.port))
780
+ uriTokens.push(port)
774
781
  }
775
782
 
776
783
  return uriTokens.length ? uriTokens.join('') : undefined
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency-free RFC 3986 URI toolbox",
4
- "version": "4.1.3",
4
+ "version": "4.1.4",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -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
+ })