Cross-site request forgery (CSRF) is a common and serious exploit where a user is tricked into performing an action he didn’t explicitly intend to do. This can happen when, for example, the user is logged in to one of his favorite websites and proceeds to click a seemingly harmless link. In the background, his profile information is silently updated with an attacker’s e-mail address. The attacker can then use the website’s password reset feature to e-mail herself a new password and she’s just successfully stolen the account. Any action that a user is allowed to perform while logged in to a website, an attacker can perform on his/her behalf, whether it’s updating a profile, adding items to a shopping cart, posting messages on a forum, or practically anything else.
If you’ve never heard of CSRF before or you haven’t written your code with prevention in mind, then I hate to break it to you but more than likely you’re vulnerable. In this guide I will show you exactly how CSRF attacks work and what you can do to protect your users.
How It Works
To understand how a CSRF attack works, it’s best to see it in action. To illustrate an attack, I’d like to create a simple example that has the ability to logout an active session. We will need a login page (login.php
), a processing script to handle logging in and logging out of the session (process.php
), and finally an example attack (harmless.html
).
First, here’s the code for login.php
:
<?php
session_start();
?>
<html>
<body>
<?php
if (isset($_SESSION["user"])) {
echo "<p>Welcome back, " . $_SESSION["user"] . "!<br>";
echo '<a href="process.php?action=logout">Logout</a></p>';
}
else {
?>
<form action="process.php?action=login" method="post">
<p>The username is: admin</p>
<input type="text" name="user" size="20">
<p>The password is: test</p>
<input type="password" name="pass" size="20">
<input type="submit" value="Login">
</form>
<?php
}
?>
</body>
</html>
The login.php
script begins by initializing the session data. It then checks to see if $_SESSION["user"]
has been set, and if so displays a welcome message along with a link to logout. Otherwise it displays the login form.
This is the processing script, process.php
:
<?php
session_start();
switch($_GET["action"]) {
case "login":
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$user = (isset($_POST["user"]) &&
ctype_alnum($_POST["user"]) ? $_POST["user"] : null;
$pass = (isset($_POST["pass"])) ? $_POST["pass"] : null;
$salt = '$2a$07$my.s3cr3t.SalTY.str1nG$';
if (isset($user, $pass) && (crypt($user . $pass, $salt) ==
crypt("admintest", $salt))) {
$_SESSION["user"] = $_POST["user"];
}
}
break;
case "logout":
$_SESSION = array();
session_destroy();
break;
}
header("Location: login.php");
?>
The process.php
script also begins by initializing the session data, and then checks to see if there is an action to work with. We perform some basic input validation using PHP’s ternary operator along with the ctype_alnum()
and crypt()
functions, and then set or destroy the session variable accordingly. The user is redirected back to login.php
at the end of the script.
Now let’s focus on the file an attacker might create to exploit the code in our previous examples. This is the exploit code, harmless.html
:
<html>
<body>
<p>This page is harmless... Or is it?</p>
<!-- Address to target website -->
<img src="process.php?action=logout" style="display: none;">
</body>
</html>
If you visit login.php
and log in to your account, and then while logged in you proceed to visit the attacker’s page, you will be automatically logged out even though you didn’t click the logout link. The browser sends a request to the server to access the process.php
script, expecting it to be an image file. The processing script has no way of differentiating between a valid request initiated by a user clicking on the logout link and a cleverly-crafted request the browser was tricked into sending.
The harmless.html
file could be hosted on an entirely different server than the one you’re logged into, and it would still work because the attacker’s page is making a request on your behalf using the session you have open in the background. It doesn’t even matter if the website you’re logged into is on a private network, the request will be submitted from your IP address as if you made the request yourself, making a trace to the source of the attack nearly impossible.
Additionally, if you allow your users to link to images as a profile avatar or the like, without proper escaping and sanitizing of the user supplied data the attack may even be possible within your own web domain.
While logging someone out of a website isn’t that impressive, harmless.html
could have just as easily contained a hidden inline frame (as opposed to an image tag) with a form that automatically submits when the page is loaded, which would make any of the attacks mentioned at the beginning of this guide fair game.
Hopefully now you understand just how serious CSRF attacks can be, so let’s take a look at how you can prevent them.
Protecting Your Users
In order to ensure that an action is actually being performed by the user rather than a third party, you need to associate it with some sort of unique identifier which can then be verified. To prevent the attack, we can modify login.php
as follows:
<?php
// make a random id
$_SESSION["token"] = md5(uniqid(mt_rand(), true));
echo '<a href="process.php?action=logout&csrf=' . $_SESSION["token"] . '">Logout</a></p>';
Then to verify the identifier, we can modify process.php
as follows:
case "logout":
if (isset($_GET["csrf"]) && $_GET["csrf"] == $_SESSION["token"]) {
$_SESSION = array();
session_destroy();
}
break;
With these simple modifications, harmless.html
will no longer work because the attacker has been given the additional task of having to guess an additional random token value. To protect forms, you can also include the identifier inside of a hidden field as follows so it is submitted along with the rest of the form data.
<input type="hidden" name="csrf" value="<?php echo $_SESSION["token"]; ?>">
In my own opinion, despite the best intentioned harassing of my esteemed friends and colleagues, I prefer to use PHP’s session_id()
rather than generating a random token since I’m not particularly fond of the “security through obscurity” approach. In addition to using session_id()
, I also use session_regenerate_id()
whenever logging in or updating sensitive information in order to mitigate the risk of any session fixation attacks, and I never append the id to a URL that will be stored in the browsers history. Arbitrarily exposing the session id more than necessary is never a good idea, but so long as you’re careful I think it’s a more elegant approach. Of course, if your website uses some type of authentication that doesn’t use sessions, then you’d need to generate your own id anyway.
Conclusion
By now you should understand the basic principles underlying a CSRF attack and what steps you can take to protect your site and your users. As Ben Franklin said, “an ounce of prevention is worth a pound of cure.” I’m sure all of us would rather take the time to make sure the code we write is secure than deal with the stress, headaches and possible lawsuits surrounding a compromise.
Image via Blazej Lyjak / Shutterstock
Martin E. Psinas is a self-taught web developer, published author, and is currently studying Japanese. For more information, visit his website.