Tutorials > Setting Up a Development Environment > Cross-site Request Forgery

10. Cross-site Request Forgery

  • This article assumes you have completed the previous tutorials up to: Cross-site Scripting
  • With the first two vulnerabilities, SQL injection and cross-site scripting, there was a common theme: thou shalt never trust user input.
  • Cross-site request forgery works differently. However, it can be demonstrated fairly easily. Add a new item, Corn, right click Remove item and choose Copy link address (may be different, depending on browser)
  • Create a new file, otherwebsite.html, and add something like this:

    <h1>Somebody Else's Website</h1>
    <img src='http://localhost/removeitem.php?item_name=Corn' style='visibility:hidden;' />
  • Now if you go to that page, localhost/otherwebsite.html, you won't see an image or anything suspicious. But if go to the Developer Tools: Network Panel, you can see that a request is being made to the removeitem.php?item_name=Corn URL.
  • And when you go back to localhost you can see Corn has been removed.
  • This is because your browser looks at the image tag and tries to load an image from the URL in the src attribute.
  • When your browser sends a request to the server for that URL, it doesn't know it's not going to get an image and the server treats it the same as any other request, in this case performing the remove item action.
  • This vulnerability can be executed against forms too.
  • So how do we protect against it? Well in practice, CSRF attacks are pretty rare, but even YouTube has had CSRF vulnerabilities.
  • There's a fairly standardized method of preventing them using what's called a CSRF token, but first we need to look at session variables.
  • In PHP you can set a special PHPSESSID cookie on user's browsers to identify them. For instance, if a website had a login system, each time a user goes to a new page, you may want to display their username in the top corner or just know if they're logged in at all.
  • We do this using session_start(). *Note: This must be called at the top of the PHP file, before any HTML is rendered. This is because it sends a cookie as part of the header and the header must be sent before the HTML.

    session_start();
  • One example of what we can do with a session is track how long a user has been on the site. The $_SESSION array will persist as long as that cookie is set. This code will set a variable $_SESSION['first_visit'] that gets set only once.

    if (!isset($_SESSION['first_visit'])) {
    	$_SESSION['first_visit'] = time();
    }
  • The function time() returns a unix timestamp for the current time.
  • Now we can use this variable to calculate how long the user has been on the site:

    $seconds = time() - $_SESSION['first_visit'];
    $hours = floor($seconds/3600); // 3600 seconds in an hour
    $seconds -= $hours*3600;
    $minutes = floor($seconds/60); // 60 seconds in a minute
    $seconds -= $minutes*60;
    echo "You have been on this site for $hours hours, $minutes minutes, and $seconds seconds.";
  • Now it will be displayed on the bottom of the page. If you tried visiting on a different browser it would have a different timer. Or if you reset your cookies it will reset the timer.
  • Okay, so what's a CSRF token and how can we use sessions to prevent CSRF attacks?
  • A CSRF token is like a secret password stored in the session variable. We then include the CSRF token as a hidden form input. This way we can differentiate between requests coming from our website and requests coming from somewhere else.
  • It is generally considered best practice not to use CSRF tokens in GET requests (URLs).
  • This means we need to update our Remove item link to use a form protected with a CSRF token. First lets initialize our CSRF token like we did our $_SESSION['first_visit'] variable:

    <?php
    if (!isset($_SESSION['csrf_token'])) {
    	$_SESSION['csrf_token'] = md5(session_id().time());
    }
    echo "Our CSRF token: " . $_SESSION['csrf_token'];
    ?>
  • And now we can see that we've generated a CSRF token that persists across any page.
  • Using md5(session_id().time()) is just a quick way of generating an unpredicatble random string. See md5() or Stack Overflow.
  • Alright, lets create our Remove item form and see if it works:

    <td>
    	<form action='removeitem.php' method='post' style='margin:0;'>
    		<input type='hidden' name='csrf_token' value='<?php echo $_SESSION['csrf_token']; ?>' />
    		<input type='hidden' name='item_name' value='<?php echo urlencode($row['item_name']); ?>' />
    		<input type='submit' value='Remove Item' />
    	</form>
    </td>
  • Also, it's best to move the token generator to the top:
  • Now if we click the new Remove item button we will still go to the removeitem.php page. But we need to update this page to handle CSRF tokens:

    session_start();
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    	if (!isset($_SESSION['csrf_token']) || $_POST['csrf_token'] != $_SESSION['csrf_token']) {
    		echo "Invalid CSRF token.";
    		exit(0);
    	}
    }
  • Also make sure to update the $_GET variables to be $_POST instead.
  • We've now protected our Remove item link and the attack example we used earlier will no longer work.
  • We should also update our Create new item form to use CSRF tokens as well.
  • Congratulations! Now our grocery list has a pretty decent level of security!

Return to Tutorials