Skip to content

Commit 64e3402

Browse files
committed
Merge pull request from GHSA-m4v8-wqvr-p9f7
Signed-off-by: Matteo Collina <[email protected]>
1 parent 723c4e7 commit 64e3402

File tree

4 files changed

+191
-6
lines changed

4 files changed

+191
-6
lines changed

lib/core/constants.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use strict'
2+
3+
/** @type {Record<string, string | undefined>} */
4+
const headerNameLowerCasedRecord = {}
5+
6+
// https://developer.mozilla.org/docs/Web/HTTP/Headers
7+
const wellknownHeaderNames = [
8+
'Accept',
9+
'Accept-Encoding',
10+
'Accept-Language',
11+
'Accept-Ranges',
12+
'Access-Control-Allow-Credentials',
13+
'Access-Control-Allow-Headers',
14+
'Access-Control-Allow-Methods',
15+
'Access-Control-Allow-Origin',
16+
'Access-Control-Expose-Headers',
17+
'Access-Control-Max-Age',
18+
'Access-Control-Request-Headers',
19+
'Access-Control-Request-Method',
20+
'Age',
21+
'Allow',
22+
'Alt-Svc',
23+
'Alt-Used',
24+
'Authorization',
25+
'Cache-Control',
26+
'Clear-Site-Data',
27+
'Connection',
28+
'Content-Disposition',
29+
'Content-Encoding',
30+
'Content-Language',
31+
'Content-Length',
32+
'Content-Location',
33+
'Content-Range',
34+
'Content-Security-Policy',
35+
'Content-Security-Policy-Report-Only',
36+
'Content-Type',
37+
'Cookie',
38+
'Cross-Origin-Embedder-Policy',
39+
'Cross-Origin-Opener-Policy',
40+
'Cross-Origin-Resource-Policy',
41+
'Date',
42+
'Device-Memory',
43+
'Downlink',
44+
'ECT',
45+
'ETag',
46+
'Expect',
47+
'Expect-CT',
48+
'Expires',
49+
'Forwarded',
50+
'From',
51+
'Host',
52+
'If-Match',
53+
'If-Modified-Since',
54+
'If-None-Match',
55+
'If-Range',
56+
'If-Unmodified-Since',
57+
'Keep-Alive',
58+
'Last-Modified',
59+
'Link',
60+
'Location',
61+
'Max-Forwards',
62+
'Origin',
63+
'Permissions-Policy',
64+
'Pragma',
65+
'Proxy-Authenticate',
66+
'Proxy-Authorization',
67+
'RTT',
68+
'Range',
69+
'Referer',
70+
'Referrer-Policy',
71+
'Refresh',
72+
'Retry-After',
73+
'Sec-WebSocket-Accept',
74+
'Sec-WebSocket-Extensions',
75+
'Sec-WebSocket-Key',
76+
'Sec-WebSocket-Protocol',
77+
'Sec-WebSocket-Version',
78+
'Server',
79+
'Server-Timing',
80+
'Service-Worker-Allowed',
81+
'Service-Worker-Navigation-Preload',
82+
'Set-Cookie',
83+
'SourceMap',
84+
'Strict-Transport-Security',
85+
'Supports-Loading-Mode',
86+
'TE',
87+
'Timing-Allow-Origin',
88+
'Trailer',
89+
'Transfer-Encoding',
90+
'Upgrade',
91+
'Upgrade-Insecure-Requests',
92+
'User-Agent',
93+
'Vary',
94+
'Via',
95+
'WWW-Authenticate',
96+
'X-Content-Type-Options',
97+
'X-DNS-Prefetch-Control',
98+
'X-Frame-Options',
99+
'X-Permitted-Cross-Domain-Policies',
100+
'X-Powered-By',
101+
'X-Requested-With',
102+
'X-XSS-Protection'
103+
]
104+
105+
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
106+
const key = wellknownHeaderNames[i]
107+
const lowerCasedKey = key.toLowerCase()
108+
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
109+
lowerCasedKey
110+
}
111+
112+
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
113+
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
114+
115+
module.exports = {
116+
wellknownHeaderNames,
117+
headerNameLowerCasedRecord
118+
}

lib/core/util.js

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { InvalidArgumentError } = require('./errors')
99
const { Blob } = require('buffer')
1010
const nodeUtil = require('util')
1111
const { stringify } = require('querystring')
12+
const { headerNameLowerCasedRecord } = require('./constants')
1213

1314
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
1415

@@ -218,6 +219,15 @@ function parseKeepAliveTimeout (val) {
218219
return m ? parseInt(m[1], 10) * 1000 : null
219220
}
220221

222+
/**
223+
* Retrieves a header name and returns its lowercase value.
224+
* @param {string | Buffer} value Header name
225+
* @returns {string}
226+
*/
227+
function headerNameToString (value) {
228+
return headerNameLowerCasedRecord[value] || value.toLowerCase()
229+
}
230+
221231
function parseHeaders (headers, obj = {}) {
222232
// For H2 support
223233
if (!Array.isArray(headers)) return headers
@@ -489,6 +499,7 @@ module.exports = {
489499
isIterable,
490500
isAsyncIterable,
491501
isDestroyed,
502+
headerNameToString,
492503
parseRawHeaders,
493504
parseHeaders,
494505
parseKeepAliveTimeout,

lib/handler/RedirectHandler.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,17 @@ function parseLocation (statusCode, headers) {
184184

185185
// https://tools.ietf.org/html/rfc7231#section-6.4.4
186186
function shouldRemoveHeader (header, removeContent, unknownOrigin) {
187-
return (
188-
(header.length === 4 && header.toString().toLowerCase() === 'host') ||
189-
(removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
190-
(unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
191-
(unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
192-
)
187+
if (header.length === 4) {
188+
return util.headerNameToString(header) === 'host'
189+
}
190+
if (removeContent && util.headerNameToString(header).startsWith('content-')) {
191+
return true
192+
}
193+
if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
194+
const name = util.headerNameToString(header)
195+
return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
196+
}
197+
return false
193198
}
194199

195200
// https://tools.ietf.org/html/rfc7231#section-6.4

test/redirect-cross-origin-header.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const { createServer } = require('node:http')
5+
const { once } = require('node:events')
6+
const { request } = require('..')
7+
8+
test('Cross-origin redirects clear forbidden headers', async (t) => {
9+
t.plan(6)
10+
11+
const server1 = createServer((req, res) => {
12+
t.equal(req.headers.cookie, undefined)
13+
t.equal(req.headers.authorization, undefined)
14+
t.equal(req.headers['proxy-authorization'], undefined)
15+
16+
res.end('redirected')
17+
}).listen(0)
18+
19+
const server2 = createServer((req, res) => {
20+
t.equal(req.headers.authorization, 'test')
21+
t.equal(req.headers.cookie, 'ddd=dddd')
22+
23+
res.writeHead(302, {
24+
...req.headers,
25+
Location: `http://localhost:${server1.address().port}`
26+
})
27+
res.end()
28+
}).listen(0)
29+
30+
t.teardown(() => {
31+
server1.close()
32+
server2.close()
33+
})
34+
35+
await Promise.all([
36+
once(server1, 'listening'),
37+
once(server2, 'listening')
38+
])
39+
40+
const res = await request(`http://localhost:${server2.address().port}`, {
41+
maxRedirections: 1,
42+
headers: {
43+
Authorization: 'test',
44+
Cookie: 'ddd=dddd',
45+
'Proxy-Authorization': 'test'
46+
}
47+
})
48+
49+
const text = await res.body.text()
50+
t.equal(text, 'redirected')
51+
})

0 commit comments

Comments
 (0)