Skip to content

Commit a29a151

Browse files
authored
Merge pull request from GHSA-3cvr-822r-rqcc
* Sanitize \r\n in headers Signed-off-by: Matteo Collina <[email protected]> * fixup, use regexp Signed-off-by: Matteo Collina <[email protected]> * fixup, handle method and path too Signed-off-by: Matteo Collina <[email protected]>
1 parent 722976c commit a29a151

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

lib/core/request.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ const {
77
const assert = require('assert')
88
const util = require('./util')
99

10+
// tokenRegExp and headerCharRegex have been lifted from
11+
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
12+
13+
/**
14+
* Verifies that the given val is a valid HTTP token
15+
* per the rules defined in RFC 7230
16+
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
17+
*/
18+
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
19+
20+
/**
21+
* Matches if val contains an invalid field-vchar
22+
* field-value = *( field-content / obs-fold )
23+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
24+
* field-vchar = VCHAR / obs-text
25+
*/
26+
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
27+
28+
// Verifies that a given path is valid does not contain control chars \x00 to \x20
29+
const invalidPathRegex = /[^\u0021-\u00ff]/
30+
1031
const kHandler = Symbol('handler')
1132

1233
const channels = {}
@@ -54,10 +75,14 @@ class Request {
5475
method !== 'CONNECT'
5576
) {
5677
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
78+
} else if (invalidPathRegex.exec(path) !== null) {
79+
throw new InvalidArgumentError('invalid request path')
5780
}
5881

5982
if (typeof method !== 'string') {
6083
throw new InvalidArgumentError('method must be a string')
84+
} else if (tokenRegExp.exec(method) === null) {
85+
throw new InvalidArgumentError('invalid request method')
6186
}
6287

6388
if (upgrade && typeof upgrade !== 'string') {
@@ -301,6 +326,10 @@ function processHeader (request, key, val) {
301326
key.toLowerCase() === 'expect'
302327
) {
303328
throw new NotSupportedError('expect header not supported')
329+
} else if (tokenRegExp.exec(key) === null) {
330+
throw new InvalidArgumentError('invalid header key')
331+
} else if (headerCharRegex.exec(val) !== null) {
332+
throw new InvalidArgumentError(`invalid ${key} header`)
304333
} else {
305334
request.headers += `${key}: ${val}\r\n`
306335
}

test/client.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,3 +1994,119 @@ function buildParams (path) {
19941994

19951995
return builtParams
19961996
}
1997+
1998+
test('\\r\\n in Headers', (t) => {
1999+
t.plan(1)
2000+
2001+
const reqHeaders = {
2002+
bar: '\r\nbar'
2003+
}
2004+
2005+
const client = new Client('http://localhost:4242', {
2006+
keepAliveTimeout: 300e3
2007+
})
2008+
t.teardown(client.close.bind(client))
2009+
2010+
client.request({
2011+
path: '/',
2012+
method: 'GET',
2013+
headers: reqHeaders
2014+
}, (err) => {
2015+
t.equal(err.message, 'invalid bar header')
2016+
})
2017+
})
2018+
2019+
test('\\r in Headers', (t) => {
2020+
t.plan(1)
2021+
2022+
const reqHeaders = {
2023+
bar: '\rbar'
2024+
}
2025+
2026+
const client = new Client('http://localhost:4242', {
2027+
keepAliveTimeout: 300e3
2028+
})
2029+
t.teardown(client.close.bind(client))
2030+
2031+
client.request({
2032+
path: '/',
2033+
method: 'GET',
2034+
headers: reqHeaders
2035+
}, (err) => {
2036+
t.equal(err.message, 'invalid bar header')
2037+
})
2038+
})
2039+
2040+
test('\\n in Headers', (t) => {
2041+
t.plan(1)
2042+
2043+
const reqHeaders = {
2044+
bar: '\nbar'
2045+
}
2046+
2047+
const client = new Client('http://localhost:4242', {
2048+
keepAliveTimeout: 300e3
2049+
})
2050+
t.teardown(client.close.bind(client))
2051+
2052+
client.request({
2053+
path: '/',
2054+
method: 'GET',
2055+
headers: reqHeaders
2056+
}, (err) => {
2057+
t.equal(err.message, 'invalid bar header')
2058+
})
2059+
})
2060+
2061+
test('\\n in Headers', (t) => {
2062+
t.plan(1)
2063+
2064+
const reqHeaders = {
2065+
'\nbar': 'foo'
2066+
}
2067+
2068+
const client = new Client('http://localhost:4242', {
2069+
keepAliveTimeout: 300e3
2070+
})
2071+
t.teardown(client.close.bind(client))
2072+
2073+
client.request({
2074+
path: '/',
2075+
method: 'GET',
2076+
headers: reqHeaders
2077+
}, (err) => {
2078+
t.equal(err.message, 'invalid header key')
2079+
})
2080+
})
2081+
2082+
test('\\n in Path', (t) => {
2083+
t.plan(1)
2084+
2085+
const client = new Client('http://localhost:4242', {
2086+
keepAliveTimeout: 300e3
2087+
})
2088+
t.teardown(client.close.bind(client))
2089+
2090+
client.request({
2091+
path: '/\n',
2092+
method: 'GET'
2093+
}, (err) => {
2094+
t.equal(err.message, 'invalid request path')
2095+
})
2096+
})
2097+
2098+
test('\\n in Method', (t) => {
2099+
t.plan(1)
2100+
2101+
const client = new Client('http://localhost:4242', {
2102+
keepAliveTimeout: 300e3
2103+
})
2104+
t.teardown(client.close.bind(client))
2105+
2106+
client.request({
2107+
path: '/',
2108+
method: 'GET\n'
2109+
}, (err) => {
2110+
t.equal(err.message, 'invalid request method')
2111+
})
2112+
})

0 commit comments

Comments
 (0)