Description
Version
v23.10.0
Platform
Tested on Linux and macOS:
Linux rimmer 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64 GNU/Linux
----
Darwin holly 24.4.0 Darwin Kernel Version 24.4.0: Wed Mar 19 21:17:25 PDT 2025; root:xnu-11417.101.15~1/RELEASE_ARM64_T6020 arm64
Subsystem
http
What steps will reproduce the bug?
Create a minimal Node HTTP server with configured headersTimeout
, requestTimeout
, and keepAliveTimeout
timers:
import { createServer } from 'node:http';
const server = createServer({
connectionsCheckingInterval: 100,
headersTimeout: 1000,
requestTimeout: 4000,
keepAliveTimeout: 5000
}, (req, res) => {
res.writeHead(404);
res.end();
});
server.listen(3000);
Send an HTTP request followed by an empty line (CRLF) to the server with the following Python client:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 3000))
# Send a minimal HTTP request to the server
client.send(b'GET / HTTP/1.1\r\nHost: localhost:3000\r\nContent-Length: 0\r\n\r\n')
# Print received responses until the server closes the connection
while True:
data = client.recv(1024 * 1024)
if data == b'': # Server closed connection
return
print(data)
# Send an empty line (CRLF)
client.send(b'\r\n')
The Python client never terminates because the server never closes the connection, despite having set all three timers (headersTimeout
, requestTimeout
, 'keepAliveTimeout`).
The empty line cancels the keepAliveTimeout
timer but does not start either the headersTimeout
or requestTimeout
timers. The server will keep the connection open forever.
A malicious client could force the server to keep the connection open indefinitely.
How often does it reproduce? Is there a required condition?
Every time. I believe this is a design flaw in NodeJS.
What is the expected behavior? Why is that the expected behavior?
If the NodeJS server has all the connection-related timers set, it should never be possible for the connection to transition into a state where none of the timers are in effect. This is what seems to happen when the client sends a CRLF after the first request on a connection. What I think happens is that NodeJS stops the keepAliveTimeout
timer upon receiving the CRLF, but does not start headersTimeout
and requestTimeout
timers until the first line of the next message arrives from the client.
I think NodeJS should NOT stop the keepAliveTimeout
timer until it really receives the beginning of the following HTTP request. In other words, receiving just CRLF (empty line) should not stop the timer.
An alternative fix would be to keep the keepAliveTimeout
as it is, and instead start the headersTimeout
and requestTimeout
timers upon receiving ANY communication from the client, including CRLF.
In any case, headerTimeout
and requestTimeout
timers should be started whenever the keepAliveTimeout
is stopped. This prevents clients from tricking the server into a timer-less connection state.
What do you see instead?
When the attached Python client is run against the attached trivial NodeJS server, the Python client never terminates, despite the server having all connection timers configured.
Additional information
No response