Cross-Site Request Forgeries

Published in PHP Architect on 13 Dec 2004

This article was originally published in PHP Architect in December 2004. It was the first comprehensive account of cross-site request forgeries — expanding on an attack concept first identified by Peter Watkins — and introduced the CSRF token as the standard defense. It remains the primary reference on the Wikipedia page for CSRF, and my token-based safeguard is now implemented by nearly every major web framework.

I later identified CSRF as the mechanism behind the Samy worm (the first widespread CSRF exploit, against Myspace in 2005), discovered and responsibly disclosed a CSRF vulnerability in Amazon.com, and identified the crossdomain.xml vulnerability that affected Flickr, YouTube, Adobe, and others.

Cross-site request forgeries (CSRF, pronounced “sea surf”) are 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 on their behalf — without their knowledge.

Where Is the Trust?

To understand CSRF, it helps to contrast it with a related but opposite attack: cross-site scripting (XSS). XSS exploits the trust a user has in a website — an attacker injects malicious code into a trusted site, which then executes in the victim’s browser. CSRF works in the opposite direction: it exploits the trust a website has in 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, 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 — it allows an attacker to bypass traditional safeguards entirely.

Sample Application

Every good example needs a sample application. 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(). 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 until it parses the HTML within the response. It is at this point that the browser requests the image, using a standard GET request. 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 — a more reliable approach is described below.

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

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

Every user that visits this page sends a request to example.org just as if they had 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 and data provided via the proper HTML form.

This is an intentional mistake worth highlighting. 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:

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 on its own.

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.

The most important thing you can do is 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 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 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 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. ?>

Summary

CSRF attacks are dangerous precisely because they are invisible — the requests originate from the victim, making them indistinguishable from legitimate traffic. This means an attacker can target sites that only the victim can access, including applications on a local network.

The defense is straightforward: include a CSRF token in every form that performs an action. Generate a unique token per session, store it server-side, and verify it on submission. An attacker who cannot read the victim’s session cannot forge a valid token.

While no safeguard can be considered absolute — an attacker could theoretically guess a valid token — this approach eliminates the vast majority of CSRF risk.

A Note on Modern Frameworks

This article was written in 2004, when CSRF protection had to be implemented manually. Today, most web frameworks implement CSRF token handling automatically:

If you are using one of these frameworks, CSRF protection is likely already in place. Understanding how it works — what the token is, why it works, and what it protects against — remains important for evaluating whether your implementation is correct and complete.

Photo by John Maeda

Chris Shiflett Boulder-based founder, designer, and developer. Co-founder of Studioworks and Schoolcase, and founder of Faculty, a product studio. Writing about building things on the web since 2000. More about Chris →