Cross-Site Request Forgeries

Published in PHP Architect on 13 Dec 2004

Welcome to another edition of Security Corner. This month's topic is cross-site request forgeries, an attack vector that enables an attacker to send arbitrary HTTP requests from a victim user. That's worth reading a couple of times, and it will likely not be until you've seen your first example attack that you can fully understand or appreciate the danger.

The typical scenario involves a victim that has an established level of privilege with the target site, and this allows an attacker to initiate unauthorized actions.

This article introduces cross-site request forgeries (CSRF, pronounced "sea surf") and provides a few simple steps to help prevent these types of attacks in your own applications.

Where Is the Trust?

CSRF attacks exploit the trust that a site has for a particular user. The site is the target of the attack, and the user is both the victim and an unknowing accomplice.

Because the victim sends the request (not the attacker), it can be very difficult to determine that the request represents a CSRF attack. In fact, if you have not taken specific steps to mitigate the risk of CSRF attacks, your applications are most likely vulnerable.

When developing an application, challenging tasks include authentication, identification, and authorization. Assuming that you have hypothetically achieved maximum security regarding these tasks, a CSRF attack can still be successful, because it allows an attacker to bypass traditional safeguards.

Sample Application

Every good example needs a sample application, and this article uses one that allows users to buy stocks. So that the actual buying process doesn't complicate the various examples, I use a hypothetical function called buy_stocks(). For my security conscious readers, you can assume that this function contains sufficient input filtering. The form handling is the important part for this discussion.

The interface that allows users to buy stocks includes an HTML form:

  1. <form action="buy.php" method="POST">
  2. <p>Symbol: <input type="text" name="symbol" /></p>
  3. <p>Shares: <input type="text" name="shares" /></p>
  4. <p><input type="submit" value="Buy" /></p>
  5. </form>

This form allows the user to specify the stock symbol and the number of shares. It sends this information to buy.php:

  1. <?php
  2.  
  3. session_start();
  4.  
  5. if (isset($_REQUEST['symbol'] &&
  6.     isset($_REQUEST['shares']))
  7. {
  8.     buy_stocks($_REQUEST['symbol'],
  9.                $_REQUEST['shares']);
  10. }
  11.  
  12. ?>

Keep this application in mind as you continue reading.

Example Exploit

The simplest example exploit uses an image. To understand the exploit, it is first necessary to understand how a browser requests images. Consider a very simple page:

  1. <html>
  2. <p>Here is my sample image:
  3. <img src="http://example.org/example.png" /></p>
  4. </html>

When a browser requests this page, it cannot know that the page has an image. The browser only realizes this once it parses the HTML within the response. It is at this point that the browser requests the image, and it uses a standard GET request to do so. This is the important characteristic. It is impossible for the target site to distinguish between a request for an image and a request for any other resource.

When requesting an image, some browsers alter the value of the Accept header to give a higher priority to image types. Resist the urge to rely upon this behavior for protection - I show you a more reliable approach at the end of this article.

Now, imagine that the image in a page is the following:

  1. <img src="http://example.org/buy.php?symbol=SCOX&shares=1000" />

Every user that visits this page sends a request to example.org just as if the user clicked a link to the same URL. Because the sample application uses $_REQUEST instead of the more specific $_POST, it cannot distinguish between data sent in the URL from data provided in the proper HTML form.

This is an intentional mistake that I want to highlight. Using $_REQUEST unnecessarily increases your risk. In addition, if you perform an action (such as buying stocks) as a result of a GET request, you are violating the HTTP specification. Section 9.1.1 of RFC 2616 states the following:

In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered "safe". This allows user agents to represent other methods, such as POST, PUT and DELETE, in a special way, so that the user is made aware of the fact that a possibly unsafe action is being requested.

POST requests can also be forged, so do not consider a strict use of $_POST to be sufficient protection.

Safeguarding Against CSRF

There are a few steps you can take to mitigate the risk of CSRF attacks. Minor steps include using POST rather than GET in HTML forms that perform actions, using $_POST instead of $_REQUEST, and requiring verification for critical actions (convenience typically increases risk, and it's up to you to decide the appropriate balance).

The most important thing you can do is to try to force the use of your own forms. If a user sends a request that looks like it is the result of a form submission, doesn't it make sense to be a little suspicious if the user has not recently requested the form?

Consider the following replacement for the HTML form in the sample application:

  1. <?php
  2.  
  3. $token = md5(uniqid(rand(), TRUE));
  4. $_SESSION['token'] = $token;
  5. $_SESSION['token_time'] = time();
  6.  
  7. ?>
  8. <form action="buy.php" method="post">
  9. <input type="hidden" name="token" value="<?php echo $token; ?>" />
  10. <p>
  11. Symbol: <input type="text" name="symbol" /><br />
  12. Shares: <input type="text" name="shares" /><br />
  13. <input type="submit" value="Buy" />
  14. </p>
  15. </form>

Because this form does not represent an entire script, I do not include the call to session_start(). You can safely assume that this required step takes place prior to the form.

With this simple modification, a CSRF attack must include a valid token (anti-CSRF token) in order to perfectly mimic the form submission. Because you store the user's token in the session, it is also necessary that the attacker uses the token unique to the victim. This effectively limits any attack to a single user, and it requires the attacker to obtain a valid token for another user (obtaining your own token is useless when it comes to forging requests from someone else).

This token should be initialized just like any other session variable:

  1. <?php
  2.  
  3. if (!isset($_SESSION['token']))
  4. {
  5.     $_SESSION['token'] = md5(uniqid(rand(), TRUE));
  6. }
  7.  
  8. ?>

The token can be checked with a simple conditional statement:

  1. <?php
  2.  
  3. if ($_POST['token'] == $_SESSION['token'])
  4. {
  5.     /* Valid Token */
  6. }
  7.  
  8. ?>

The validity of the token can also be limited to a small window of time, such as five minutes:

  1. <?php
  2.  
  3. $token_age = time() - $_SESSION['token_time'];
  4.  
  5. if ($token_age <= 300)
  6. {
  7.     /* Less than five minutes has passed. */
  8. }
  9.  
  10. ?>

Until Next Time...

CSRF attacks are very dangerous, and most applications that do not take specific steps to prevent CSRF attacks are vulnerable. Because the requests originate from the victim, it is possible for an attacker to target sites that only the victim can access, such as ones on a local network.

If you use a token in all of your forms as I have suggested, you can eliminate CSRF from your list of concerns. While no safeguard can be considered absolute (an attacker can theoretically guess a valid token), this approach mitigates the majority of the risk. Until next month, be safe.