HTTP Response Splitting
Published in PHP Architect on 25 Oct 2005HTTP response splitting derives its name from the technique of splitting a single HTTP response into two or more responses. This particular technique is best explained with an example and an examination of the underlying HTTP transactions. Consider this example that redirects the user:
header("Location: {$_GET['url']}");
Although the use of $_GET['url']
makes the use of tainted data more obvious, be aware that any tainted data used in this way yields the
same vulnerability. Another common example is the use of $_SERVER['PHP_SELF']
.
This script can be referenced in links that include the target URL in the query string:
http://example.org/redirect.php?url=http%3A%2F%2Fexample.org%2Ftarget.php
In order to understand how this approach can be abused, it’s important to understand the expected behavior. In this case, the response that the server sends is something similar to the following:
HTTP/1.1 302 Found
Server: Apache/1.3.33 (Debian GNU/Linux)
Location: http://example.org/target.php
Content-Length: 0
A browser that receives such a response transparently
requests the new resource (indicated in the Location
header), and it is the response to this second request that is actually
rendered in the browser. The important thing to notice in this response is that
the value of the Location header comes directly from
the value of $_GET['url']
.
The risks here aren’t much different than that of SQL injection or XSS (cross-site scripting) — an attacker is given the opportunity to modify a string, and the context in which that string is used defines the types of attacks that are possible. In this case, an attacker can modify the structure of the HTTP response. One possible attack—the one from which the name is derived—is the following:
HTTP/1.1 302 Found
Server: Apache/1.3.33 (Debian GNU/Linux)
Location: http://example.org/target.php
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 34
<html><p>Forged Content</p></html>
Content-Length: 0
The highlighted portion represents the attacker’s contribution. Instead of providing a URL as expected, the attacker provides a URL and a complete second response, and this response is rendered in the browser. Worse, the browser believes that both responses are provided by the vulnerable site, and the user has no way to know that the content is not legitimate.
This attack is launched with a very long URL that provides
the highlighted portion as the value of $_GET['url']
:
http://example.org/redirect.php?url=http%3A%2F%2Fexample.org%2Ftarget.php%0D%0A%0D%0AHTTP%2F1.1+200+OK%0D%0AContent-Type%3A+text%2Fhtml%0D%0AContent-Length%3A+34%0D%0A%3Chtml%3E%3Cp%3EForged+Content%3C%2Fp%3E%3C%2Fhtml%3E
This particular attack does not work on all platforms. Some HTTP agents read a response into a buffer, and that buffer is discarded once the initial response is processed. However, it is important to realize that an attack is still possible — the attack needs to take this behavior into account and provide an initial response that is exactly the size of the buffer. Some trial and error might be necessary, and the attack is more difficult, but this difficulty does not provide adequate protection.
Every character in the attack can be URL encoded in order to obscure it further. Thus, even a user who might notice suspicious content in the target URL can become a victim.
You can add the following line of code at the end of a vulnerable script to get an idea of what the attack does:
error_log(print_r(headers_list(), TRUE), 3, '/path/to/http.log');
This logs the headers to be sent in the following format (the result of the example attack shown):
Array
(
[0] => X-Powered-By: PHP/5.0.5
[1] => Location: http://example.org/target.php
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 34
<html><p>Forged
Content</p></html>
)
Notice that the second element in the array contains
much more than the expected Location
header.
HTTP Header Injection
Another common application of this flaw is to inject
HTTP headers into the response. This particular attack is also much easier and
more reliable than the previous example. The most common form of HTTP header injection
is to use it to set a cookie by injecting a Set-Cookie
header. This can aid in session fixation.
The attack itself is very similar to the previous example:
http://example.org/redirect.php?url=http%3A%2F%2Fexample.org%2Ftarget.php%0D%0ASet-Cookie%3A+PHPSESSID%3D1234
This results in a response that sets a cookie of the attacker’s choosing:
HTTP/1.1 302 Found
Server: Apache/1.3.33 (Debian GNU/Linux)
Location: http://example.org/target.php
Set-Cookie: PHPSESSID=1234
Content-Length: 0
As a result, the victim’s session identifier becomes known by the attacker.
Cache Poisoning
The attacks that have been demonstrated use malicious data embedded in a URL. These assume that the attacker provides such a URL in a link, and a victim follows that link. There is a different type of attack that is possible if the attacker visits the URL: cache poisoning.
The approach is exactly the same, but the attacker uses an HTTP cache that is shared by others. By tricking the cache into believing the attacker’s response (the one with forged content as demonstrated earlier) is the legitimate response, other users might receive the forged copy in response to requests for the same URL. This can dramatically increase the magnitude of the attack.
An attacker can make a cache poisoning attack more damaging by also injecting some caching headers that allow the attacker’s response to be cached for a greater period of time.
The Countermeasure
The attacks that have been demonstrated prey upon the fact that tainted data is used. As regular Security Corner readers know, you should always filter input, and these attacks just demonstrate one more way that a failure to filter input can be leveraged by an attacker.
Your filtering should be as strict as possible, and a whitelist approach (where you err on the side of caution) is safest.
In addition to filtering, a good defense in depth approach is to inspect data for the presence of newlines and carriage returns:
<?php
if (strpos($_GET['url'], "\r") !== FALSE) {
/* URL contains a carriage return. */
}
if (strpos($_GET['url'], "\n") !== FALSE) {
/* URL contains a newline. */
}
?>
This should not be considered a substitute for filtering, but strict filtering rules in addition to this secondary inspection can safely eliminate the attacks that have been demonstrated in this article.
Until Next Time…
I hope you appreciate the dangers in using tainted data and will take steps necessary to ensure that you only use filtered data in your PHP applications. HTTP response splitting represents one of many categories of attacks that are possible because of the rampant use of tainted data in PHP applications.
Until next month, be safe.