HTTP Response Splitting

Published in PHP Architect on 25 Oct 2005

HTTP 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:

  1. 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:

  1. 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:

  1. HTTP/1.1 302 Found
  2. Server: Apache/1.3.33 (Debian GNU/Linux)
  3. Location: http://example.org/target.php
  4. 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:

  1. HTTP/1.1 302 Found
  2. Server: Apache/1.3.33 (Debian GNU/Linux)
  3. Location: http://example.org/target.php
  4.  
  5. HTTP/1.1 200 OK
  6. Content-Type: text/html
  7. Content-Length: 34
  8.  
  9. <html><p>Forged Content</p></html>
  10.  
  11. 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']:

  1. 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:

  1. 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):

  1. Array
  2. (
  3.   [0] => X-Powered-By: PHP/5.0.5
  4.   [1] => Location: http://example.org/target.php
  5.  
  6. HTTP/1.1 200 OK
  7. Content-Type: text/html
  8. Content-Length: 34
  9. <html><p>Forged
  10. Content</p></html>
  11. )

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:

  1. 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:

  1. HTTP/1.1 302 Found
  2. Server: Apache/1.3.33 (Debian GNU/Linux)
  3. Location: http://example.org/target.php
  4. Set-Cookie: PHPSESSID=1234
  5. 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:

  1. <?php
  2.  
  3. if (strpos($_GET['url'], "\r") !== FALSE) {
  4.   /* URL contains a carriage return. */
  5. }
  6.  
  7. if (strpos($_GET['url'], "\n") !== FALSE) {
  8.   /* URL contains a newline. */
  9. }
  10.  
  11. ?>

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.