All your pants are in danger - CSRF explained
Cross Site Request Forgery (CSRF) is a common form of attack against a web application. This post tries to shed some light on how it works and how exploits can be crafted. We will tailor this to a Drupal use case and example code.
Some general considerations:
- First, keep in mind how web browsers and authentication works: if you are logged-in on a site with cookies, then your browser will always send along the cookies with any request it makes to that site. If another site embeds resources from that site (images, javascript, iframes ...), then the resource will be requested with your cookies attached.
- Second, it is important to know that CSRF can only be applied to write operations. That means that a page callback must change the application's data. Example: the path /node/1 cannot be vulnerable to CSRF exploits in a standard Drupal installation, because a node is only displayed and not changed.
- Third, you must know that a CSRF attack is always performed on behalf of a particular user. That means that the attack is performed "blindly", which means that the original attacker cannot access the response during the attack.
Confused? Me too, let's look at a "real" Drupal example.
The simple GET case
This pattern is probably most widely known among developers. Here's example module code that provides a menu callback to delete some example pants data:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Implements hook_menu().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_menu</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'mymodule/pants/%/delete'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Delete pants'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'mymodule_delete_pants'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">2</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'access arguments' </span><span style="color: #007700">=> array(</span><span style="color: #DD0000">'delete pants objects'</span><span style="color: #007700">),<br> );<br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">;<br>}<p></p></span><span style="color: #FF8000">/**<br> * Page callback to delete pants.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">mymodule_delete_pants</span><span style="color: #007700">(</span><span style="color: #0000BB">$pants_id</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">db_delete</span><span style="color: #007700">(</span><span style="color: #DD0000">'mymodule_pants'</span><span style="color: #007700">)-></span><span style="color: #0000BB">condition</span><span style="color: #007700">(</span><span style="color: #DD0000">'pants_id'</span><span style="color: #007700">, </span><span style="color: #0000BB">$pants_id</span><span style="color: #007700">)-></span><span style="color: #0000BB">execute</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Deleted pants %id'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'%id' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$pants_id</span><span style="color: #007700">)));<br>}<br></span><span style="color: #0000BB">?></span></span>
Pretty straight forward, right? What could possibly go wrong with that code? The menu entry is protected with a permission, so everything is fine?
Wrong. The usual HTML snippet for exploiting CSRF here looks like this (example.com is your Drupal site):
<img src="http://example.com/mymodule/pants/1337/delete">
Chain of an attack
- The attacker posts a comment on your site: "Look what a nice picture I found" and the image snippet from above.
- You are logged in as admin and you see that there is a new comment, so you go to that comment page.
- Your browser renders the content, sees the image and tries to fetch it.
- Your browser sends the cookies along, so permission is granted to the page callback (you as admin are allowed to delete pants).
- The delete query is executed and pants with ID 1337 are removed.
- Your browser receives an HTML response from Drupal and cannot display it. It will show a broken image or similar.
- You think: "What a dumb comment, the image does not even work, LOL!"
- Later you realize that somehow the 1337 pants are gone, but you have no idea why.
The attacker does not even have to post the malicious HTML snippet on your site, she can also post it on her own site, or to Facebook for example. She only needs you to load the image or follow the link, and all your pants are in danger.
Solution
There are two common strategies to solve this problem:
- Confirmation form: the action is not performed immediately and the user has to press an extra button in order to proceed with deleting pants.
- Security Tokens: the menu link has an additional security token in its path that is validated before executing the action. drupal_get_token() and drupal_valid_token() can generate/validate the tokens for you and when the link is displayed the token is added. An attacker cannot know your token, so the validation will fail and access will be denied in that case.
A typical use case for such action links that have to be protected is implemented in the Rules Link module that we heavily use for our Recruiter and jobiqo products. I would recommend to look at its hook_menu() implementations and access/page callbacks for examples for confirmation forms and token protection. There is also an interesting Drupal Scout article about how to protect your Drupal site against CSRF.
More advanced POST cases
I will follow-up with CSRF examples for POST requests in a future blog post. Stay tuned!