Last Updated: 2013-04-03 17:58:20 UTC
by Johannes Ullrich (Version: 1)
One of the topics we cover in our "Defending Web Applications" class is how to secure static files. For example, you are faced with multiple PDFs with confidential information, and you need to integrate authorization to read these PDFs into your web application. The standard solution involves two steps:
- Move the file out of the document root
- create a script that will perform the necessary authorization and then stream the file back to the user
Typically, the process of streaming the file back to the user is pretty simple. Most languages offer the ability to read the file, and then echo it back to the browser. In some cases, like for example PHP, there is a special command for this (readfile). This makes writing these access control scripts pretty easy, until you are faced with a new twist, the "Range" header.
The "Range" header is meant to be used to support partial downloads. A client may request just part of a file, instead of asking for the entire file.
RFC 2616 is a bit ambiguous when it comes to "Range" headers. First of all, it introduces the "Accept-Ranges" header, which can be used by the server to signal that it supports the "Range" header. Next, it states that the client may send a request using a "Range" header anyway, even if the server doesn't advertise support for it. The server also has the option to send "Accept-Ranges: none" to explicitly state that it does not support this type of header.
So what's the problem? It turns out that different HTTP clients appear to deal with "Range" headers slightly differently. In particular the iOS Podcast client requires support for the Range header, and will only download parts of the file if they are not supported. Apple recently advised iTunes publishers of this issue and requires content to be hosted on servers that support the Range header.
For a server, this is usually not a problem, wouldn't it be for a recent Apache DoS attack that caused some to block Range requests. Also, our "file streaming" script now needs to support the range requests.
Here is a quick outline of how to support "Range" requests properly:
Figure out if the Range header is used and extract the requested range. The range header should look like:
but could look like:
If the upper end is missing, it is assumed to empty "until the end of the file".
load the file (if possible, only the part that needs to be send)
Send the file, but use a "206 Partial Content" response code. Also, add the "Content-Range" header to indicate what you are sending.
Content-Range: bytes 1234-5678/1234567 (start-end/total size). One interesting twist: The "size" is indicated in bytes, while the range is indicated as an offset. So the maximum "Range" is the size-1.
Aside from the annoyance of having to write a more complex script, why does this matter for security?
Think Intrusion Detection systems, and maybe even web application firewalls: It is now for example possible for an attacker to request your secret document one byte at a time, possibly defeating data leakage protection. Or an attacker streaming an exploit from a web server could do so in small chunks to again defeat content filtering by the client. I played with various overlapping ranges and such, and it looks like browsers will discard these requests as they should.
It is also possible to specify multiple ranges in one request (which is what the Apache DoS was about), but so far I haven't observed any requests like this.
In short: watch it but don't block it. It may make sense to log and pay attention to Range requests, but you shouldn't blindly block all of them as they may be required by the browser/http client.
RFC 2616: http://www.rfc-editor.org/rfc/rfc2616.txt