Skip to content

Commit 3219a3f

Browse files
XvezdaMert Can Altin
authored and
Mert Can Altin
committed
fix(fetch): properly redirect non-ascii location header url (nodejs#2971)
* fix(fetch): properly redirect non-ascii location header url * chore: fix typo * test: use simpler code * chore: clarify what the code does * chore: add comment * chore: normalize location url only if it contains invalid character * chore: apply suggestion See: nodejs#2971 (comment) * chore: remove redundant condition check
1 parent 7bdb424 commit 3219a3f

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

lib/web/fetch/util.js

+36
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ function responseLocationURL (response, requestFragment) {
4444
// 3. If location is a header value, then set location to the result of
4545
// parsing location with response’s URL.
4646
if (location !== null && isValidHeaderValue(location)) {
47+
if (!isValidEncodedURL(location)) {
48+
// Some websites respond location header in UTF-8 form without encoding them as ASCII
49+
// and major browsers redirect them to correctly UTF-8 encoded addresses.
50+
// Here, we handle that behavior in the same way.
51+
location = normalizeBinaryStringToUtf8(location)
52+
}
4753
location = new URL(location, responseURL(response))
4854
}
4955

@@ -57,6 +63,36 @@ function responseLocationURL (response, requestFragment) {
5763
return location
5864
}
5965

66+
/**
67+
* @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
68+
* @param {string} url
69+
* @returns {boolean}
70+
*/
71+
function isValidEncodedURL (url) {
72+
for (const c of url) {
73+
const code = c.charCodeAt(0)
74+
// Not used in US-ASCII
75+
if (code >= 0x80) {
76+
return false
77+
}
78+
// Control characters
79+
if ((code >= 0x00 && code <= 0x1F) || code === 0x7F) {
80+
return false
81+
}
82+
}
83+
return true
84+
}
85+
86+
/**
87+
* If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
88+
* Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
89+
* @param {string} value
90+
* @returns {string}
91+
*/
92+
function normalizeBinaryStringToUtf8 (value) {
93+
return Buffer.from(value, 'binary').toString('utf8')
94+
}
95+
6096
/** @returns {URL} */
6197
function requestCurrentURL (request) {
6298
return request.urlList[request.urlList.length - 1]

test/fetch/fetch-url-after-redirect.js

+21
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,24 @@ test('after redirecting the url of the response is set to the target url', async
3838

3939
assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`)
4040
})
41+
42+
test('location header with non-ASCII character redirects to a properly encoded url', async (t) => {
43+
// redirect -> %EC%95%88%EB%85%95 (안녕), not %C3%AC%C2%95%C2%88%C3%AB%C2%85%C2%95
44+
const server = createServer((req, res) => {
45+
if (res.req.url.endsWith('/redirect')) {
46+
res.writeHead(302, undefined, { Location: `/${Buffer.from('안녕').toString('binary')}` })
47+
res.end()
48+
} else {
49+
res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' })
50+
res.end()
51+
}
52+
})
53+
t.after(closeServerAsPromise(server))
54+
55+
const listenAsync = promisify(server.listen.bind(server))
56+
await listenAsync(0)
57+
const { port } = server.address()
58+
const response = await fetch(`http://127.0.0.1:${port}/redirect`)
59+
60+
assert.strictEqual(response.url, `http://127.0.0.1:${port}/${encodeURIComponent('안녕')}`)
61+
})

0 commit comments

Comments
 (0)