Skip to content

Commit c88b59e

Browse files
committed
fix: normalize decoded reg-name case
Signed-off-by: Matteo Collina <[email protected]>
1 parent 412e40a commit c88b59e

2 files changed

Lines changed: 48 additions & 6 deletions

File tree

‎index.js‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -526,13 +526,17 @@ function parseWithStatus (uri, opts) {
526526
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
527527
}
528528

529-
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
530-
if (uri.indexOf('%') !== -1) {
531-
if (parsed.host !== undefined && !malformedIPLiteral) {
532-
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
533-
parsed.host = reescapeHostDelimiters(host, isIP)
534-
}
529+
if (uri.indexOf('%') !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
530+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
531+
if (!isIP) {
532+
// Fold reg-name case after decoding unreserved octets. The second
533+
// pass only restores uppercase hex in escapes that remain encoded.
534+
host = normalizePercentEncoding(host.toLowerCase())
535535
}
536+
parsed.host = reescapeHostDelimiters(host, isIP)
537+
}
538+
539+
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
536540
if (parsed.path) {
537541
parsed.path = normalizePathEncoding(parsed.path)
538542
}

‎test/security-normalization.test.js‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,44 @@ test('hostname normalization decodes only current unreserved escapes', (t) => {
126126
t.end()
127127
})
128128

129+
test('scheme-relative hostname normalization folds decoded ASCII case', (t) => {
130+
const encodedHost = '//%41.com'
131+
const normalizedHost = fastURI.normalize(encodedHost)
132+
const encodedMetadata = '//%4Detadata.internal/private'
133+
const literalMetadata = '//metadata.internal/private'
134+
135+
t.equal(fastURI.parse(encodedHost).host, 'a.com', 'parse folds an encoded uppercase host letter')
136+
t.equal(normalizedHost, '//a.com', 'normalize emits a lowercase host')
137+
t.equal(fastURI.normalize(normalizedHost), normalizedHost, 'host normalization is idempotent')
138+
t.equal(fastURI.equal(encodedHost, '//a.com'), true, 'encoded and literal host spellings compare equal')
139+
t.equal(fastURI.equal(encodedMetadata, literalMetadata), true, 'encoded metadata host compares equal')
140+
t.equal(
141+
fastURI.resolve('x://trusted.example/', encodedMetadata),
142+
fastURI.resolve('x://trusted.example/', literalMetadata),
143+
'encoded and literal metadata hosts resolve identically'
144+
)
145+
t.equal(
146+
fastURI.normalize('//example.com%2fpath'),
147+
'//example.com%2Fpath',
148+
'reserved host escapes remain encoded with uppercase hex'
149+
)
150+
t.equal(fastURI.normalize('//%2541.com'), '//%2541.com', 'nested host escapes are not decoded twice')
151+
t.equal(fastURI.equal('//User@%41.com/path', '//[email protected]/path'), false, 'userinfo remains case-sensitive')
152+
t.equal(fastURI.equal('//%41.com/Path', '//a.com/path'), false, 'path remains case-sensitive')
153+
t.equal(fastURI.equal('//%41.com/?Token=Value', '//a.com/?token=value'), false, 'query remains case-sensitive')
154+
t.end()
155+
})
156+
157+
test('host normalization applies to schemes that skip path normalization', (t) => {
158+
const encodedHost = 'mailto://%41.com'
159+
const literalHost = 'mailto://a.com'
160+
161+
t.equal(fastURI.parse(encodedHost).host, fastURI.parse(literalHost).host, 'parse results have consistent hosts')
162+
t.equal(fastURI.normalize(encodedHost), fastURI.normalize(literalHost), 'normalize results are consistent')
163+
t.equal(fastURI.equal(encodedHost, literalHost), true, 'encoded and literal hosts compare equal')
164+
t.end()
165+
})
166+
129167
test('host conversion failures are not treated as comparable URLs', (t) => {
130168
const malformedHost = 'http://trusted.test%2540evil.test/'
131169

0 commit comments

Comments
 (0)