Range Request DoS: An uncontrolled memory consumption vector in Go's net/http
In this blog I highlight a very old DoS technique, which works via usage of HTTP's range requests, which I have dubbed RRDoS for the rest of the post [short for Range Request Denial of Service]. This is not to be confused with ReDoS, which depends on misconfigured regex.
This simply serves any content inside the /static/ directory.
What are Range requests?
Let's first understand what range requests are. Simply put by MDN- HTTP range requests allow to send only a portion of an HTTP message from a server to a client. Partial requests are useful for large media or downloading files with pause and resume functions, for example.
As an example, if there is an endpoint which serves an image at path /static/cat.jpg whose size is 1024 bytes, then we can request the first 10 bytes only by sending a request like:
GET /static/cat.jpg HTTP/1.1
Host: www.test.com
Range: bytes=0-10
Now if the server accepts range requests, it will reply with something like:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-10/1024
Content-Length: 10
Now that we know what range requests are, let's try to understand the attack.
Multipart ranges
Using range requests, it is also possible for one to request multiple ranges for a resource, by sending a request like:
GET /static/cat.jpg HTTP/1.1
Host: www.test.com
Range: bytes=0-10,10-1024
The server will respond to this request by sending:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282
--3d6b6a416f9b5
Content-Type: image/jpeg
Content-Range: bytes 0-10/1024
<binary content here>
--3d6b6a416f9b5
Content-Type: image/jpeg
Content-Range: bytes 10-1024/1024
<binary content here>
--3d6b6a416f9b5--
The attack
RFC 7233 lays down the rules and semantics for use of range requests by servers and clients as well. If you scroll down to the Security Considerations section, you will see a small paragraph which says:
" Unconstrained multiple range requests are susceptible to denial-of- service attacks because the effort required to request many overlapping ranges of the same data is tiny compared to the time, memory, and bandwidth consumed by attempting to serve the requested data in many parts. Servers ought to ignore, coalesce, or reject egregious range requests, such as requests for more than two overlapping ranges or for many small ranges in a single set, particularly when the ranges are requested out of order for no apparent reason. Multipart range requests are not designed to support random access. "
This is self-explanatory. Basically, if you request many overlapping ranges recursively, the server may try to parse EACH of these ranges individually, leading to a potential memory consumption, and eventually crashing the server.
An example malicious request for the cat.jpg file above would look like:
GET /static/cat.jpg HTTP/1.1
Host: www.test.com
Range: bytes=0-5,0-7,1-6,9-12,10-11,4-5,0-9,0-10,0-12,...
If the server is vulnerable to the above attack, it creates pieces of a multipart response for each range specified. That eats up both memory and CPU on the server, and doing so tens or hundreds of times for multiple attacker connections is enough to exhaust the server resources and cause the DoS. The range requests in the attack are obviously not reasonable, but they are legal according to the HTTP specification. Apache and Squid have been found vulnerable to this attack in the past. (reference here)
Attacking Go's net/http
Now that we know what a textbook RRDoS attack request looks like, let's try to reproduce it in Go's net/http package.
We create a server with the following code:
- http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./")))) |
This simply serves any content inside the /static/ directory.
Now when we try to send a malicious range request to the /static directory using a lot of concurrent threads, we are clearly able to see that the server lags out quite a lot in response to it. However, when we read the response that we finally get, we see that the server did NOT send a response to the Range request. It merely sends us the complete image file, without the multipart stuff.
If this is happening, and the server isn't responding to the Range request, then why does the server take so much time to respond to the request?
The answer lies in the code written in Go for handling Range requests in the net/http/fs.go file.
If you see, on line 10 of the above code, there indeed exists a check for this exact attack by use of the sumRangesSize() function. Then why are we still getting uncontrolled memory consumption?
So whats happening is, on line 2 of the above code, there is a function called parseRange(), which is still parsing the Range request sent. If you look at the code for the parseRange() function, it processes the values in the Range header, and simultaneously creates an array of type []httpRange, whose length is equal to the different ranges specified in the Range header (eg. 1-5, 20-25 etc.) to hold the requested bytes.
Even though these bytes aren't pushed to the array, the array is still declared as a placeholder for these values, which takes up a lot of memory allocation.
And this is why we see a rise in the memory consumption of the server, when sending a lot of these malicious Range requests on several concurrent threads to a server.
So whats happening is, on line 2 of the above code, there is a function called parseRange(), which is still parsing the Range request sent. If you look at the code for the parseRange() function, it processes the values in the Range header, and simultaneously creates an array of type []httpRange, whose length is equal to the different ranges specified in the Range header (eg. 1-5, 20-25 etc.) to hold the requested bytes.
Even though these bytes aren't pushed to the array, the array is still declared as a placeholder for these values, which takes up a lot of memory allocation.
And this is why we see a rise in the memory consumption of the server, when sending a lot of these malicious Range requests on several concurrent threads to a server.