The timeout logic is the simplest of the typical SlowLoris attack countermeasures and thus it is what Node.js implements. But if we look at the history of the Node.js’ changes, something very interesting shows up.
As previously stated, the SlowLoris attack has been known since 2008. Node.js, however, was completely vulnerable (at least with the default configuration) to this attack up to version 10.14.0, which was released on November 28th, 2018 (so ten years after it was introduced).
In that release a new timeout
http.Server.headersTimeout was introduced. This timeout represents the maximum time given to a client to completely transfer the request’s header to the server. The default value is 60 seconds. Unfortunately, this is ineffective against SlowLoris as the body of the request is not considered.
In 13.0.0, released on October 22nd, 2019, the default for
http.Server.timeout (which is the maximum amount of time a socket can stay open without transferring any data) was changed from 120 seconds (2 minutes) to no timeout. This made the situation worse because once the headers had been sent, attacking clients had no minimum speed required to send the body.
The Node.js’ team's choice for backward compatibility was to leave frameworks the responsibility to mitigate the problem. As Open Source projects have several priorities and limited development resources, this never happened.
An additional timeout
http.Server.requestTimeout was introduced in Node.js 14.11.0, released on September 15th, 2020. This timeout represents the maximum time given to a client to completely transfer the request to the server and it comprehensively implements the third countermeasure described in the previous section.
Unfortunately, in order to avoid breaking the semantic versioning contract Node.js adheres to, this feature had to be backward compatible, which meant keeping this feature disabled by default. So, once again, Node.js was still not protected by SlowLoris attacks.
So, up to Node.js 16 there was no defense from SlowLoris attacks using the default configuration. Instead, developers had to create an application that explicitly set
http.Server.requestTimeout to successfully prevent SlowLoris attacks. This was unnecessary for most applications as they are typically deployed behind a reverse proxy that ships its own mitigations.
The story does not end here. As previously stated, in the default configuration the
http.Server.requestTimeout is set to 0 while
http.Server.headersTimeout (which becomes the only active defense against SlowLoris) is set to 60 seconds.
Unfortunately, the default implementation of that timeout checking had a hidden problem. Node.js is always very careful about adding changes that negatively affect performance, and thus the feature was implemented in the most performant way possible, as it can be seen here.
It is not immediately visible, but the timer is verified after new data is received from the socket. This means that a connection can lower the transmission rate of the headers up to
http.Server.timeout - 1 milliseconds.
This can be easily verifiable by executing the snippet below: