Skip to content

Commit

Permalink
fix(fetch): properly redirect non-ascii location header url (#2971)
Browse files Browse the repository at this point in the history
* 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: #2971 (comment)

* chore: remove redundant condition check
  • Loading branch information
Xvezda authored Mar 21, 2024
1 parent 2252a5f commit 1b62571
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 0 deletions.
36 changes: 36 additions & 0 deletions lib/web/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ function responseLocationURL (response, requestFragment) {
// 3. If location is a header value, then set location to the result of
// parsing location with response’s URL.
if (location !== null && isValidHeaderValue(location)) {
if (!isValidEncodedURL(location)) {
// Some websites respond location header in UTF-8 form without encoding them as ASCII
// and major browsers redirect them to correctly UTF-8 encoded addresses.
// Here, we handle that behavior in the same way.
location = normalizeBinaryStringToUtf8(location)
}
location = new URL(location, responseURL(response))
}

Expand All @@ -57,6 +63,36 @@ function responseLocationURL (response, requestFragment) {
return location
}

/**
* @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
* @param {string} url
* @returns {boolean}
*/
function isValidEncodedURL (url) {
for (const c of url) {
const code = c.charCodeAt(0)
// Not used in US-ASCII
if (code >= 0x80) {
return false
}
// Control characters
if ((code >= 0x00 && code <= 0x1F) || code === 0x7F) {
return false
}
}
return true
}

/**
* If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
* Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
* @param {string} value
* @returns {string}
*/
function normalizeBinaryStringToUtf8 (value) {
return Buffer.from(value, 'binary').toString('utf8')
}

/** @returns {URL} */
function requestCurrentURL (request) {
return request.urlList[request.urlList.length - 1]
Expand Down
21 changes: 21 additions & 0 deletions test/fetch/fetch-url-after-redirect.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,24 @@ test('after redirecting the url of the response is set to the target url', async

assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`)
})

test('location header with non-ASCII character redirects to a properly encoded url', async (t) => {
// redirect -> %EC%95%88%EB%85%95 (안녕), not %C3%AC%C2%95%C2%88%C3%AB%C2%85%C2%95
const server = createServer((req, res) => {
if (res.req.url.endsWith('/redirect')) {
res.writeHead(302, undefined, { Location: `/${Buffer.from('안녕').toString('binary')}` })
res.end()
} else {
res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' })
res.end()
}
})
t.after(closeServerAsPromise(server))

const listenAsync = promisify(server.listen.bind(server))
await listenAsync(0)
const { port } = server.address()
const response = await fetch(`http://127.0.0.1:${port}/redirect`)

assert.strictEqual(response.url, `http://127.0.0.1:${port}/${encodeURIComponent('안녕')}`)
})

0 comments on commit 1b62571

Please sign in to comment.