In today's tutorial, we will learn how to painlessly protect your CodeIgniter (pre 2.0) application against Cross-Site Request Forgery attacks. The library we'll be creating today will automate all of the protection mechanisms, making your site stronger and more secure.
Step 1 - Understanding the Attack Vector
Cross-Site Request Forgery attacks are based on unprotected forms on your sites.
An attacker could create a bogus form on his site - for example a search form. This form could have hidden inputs that contain malicious data. Now the form isn't actually sent to the attacker's site to perform the search; in reality, the form points to your site! Since your website will trust that the form is genuine, it goes through and executes the requested (and perhaps malicious) actions.
Imagine that a user is logged into your site, and is redirected to the attacker's site for some reason (phishing, XSS, you name it). The attacker's form could point to your account deletion form on your site. If the user performs a "search" on the attackers site, his account will then be deleted without him knowing!
There are numerous ways to prevent these sorts of attacks.
- Check the HTTP Referer header and see if it belongs to your site. The problem with this method is that not all browsers submit this header (I personally had this problem once with IE7); it could be forged anyway.
- Another method (the one we will use), is to include a random string (a "token") on each form, and store that token on the user's session. On each
POST
request, compare the submitted token to the one on store, and, if they differ, deny the request. Your site still needs to be protected against XSS though, because if it's not, this method becomes useless.
Step 2 - Planning
We'll need to do three things for each request:
- If the request is a
POST
request, validate that the submitted token. - Generate a token in case there isn't one.
- Inject the token into all forms. This makes the method seamless and painless, since no modification is needed on your views.
To do this automatically, we'll use CodeIgniter hooks. Hooks allow us to execute all actions on different parts of the request. We'll need three:
- post_controller_constructor - To check the submitted token, we'll need a post_controller_constructor hook. Hooking this action before this event doesn't give us access to CodeIgniter's instance correctly.
- Generate the Token - To generate the token, we'll use the same hook as before. This allows us to have access to it in case we needed to print it manually in our views.
- display_override - To inject the token automatically in our views, we'll need to use the display_override hook. This is a tricky hook though, since, as its name implies, it overrides the display of the views. We need to output the content ourselves if we use this hook (check the CodeIgniter documentation for more info).
Step 3 - Token Generation
Let's get started. We'll go step by step in order to explain everything as thoroughly as possible. We'll create the method that generates the token first, so we can test everything correctly afterwards. Create a file in your system/application/hooks
folder called "csrf.php
", and paste the following code:
<?php /** * CSRF Protection Class */ class CSRF_Protection { /** * Holds CI instance * * @var CI instance */ private $CI; /** * Name used to store token on session * * @var string */ private static $token_name = 'li_token'; /** * Stores the token * * @var string */ private static $token; // ----------------------------------------------------------------------------------- public function __construct() { $this->CI =& get_instance(); } }
Hopefully, what we've added above should look rather basic to you. We're creating a class, called CSRF_Protection
, an instance variable to hold the CodeIgniter instance, two static variables to hold the name of the parameter that will store the token, and one to store the token itself for easy access throughout the class. Within the class constructor (__construct
), we simply retrieve the CodeIgniter instance, and store it in our corresponding instance variable.
Note: The "name of the parameter" is the name of the field that holds the token. So on the forms, it's the name of the hidden input.
After the class constructor, paste the following code:
/** * Generates a CSRF token and stores it on session. Only one token per session is generated. * This must be tied to a post-controller hook, and before the hook * that calls the inject_tokens method(). * * @return void * @author Ian Murray */ public function generate_token() { // Load session library if not loaded $this->CI->load->library('session'); if ($this->CI->session->userdata(self::$token_name) === FALSE) { // Generate a token and store it on session, since old one appears to have expired. self::$token = md5(uniqid() . microtime() . rand()); $this->CI->session->set_userdata(self::$token_name, self::$token); } else { // Set it to local variable for easy access self::$token = $this->CI->session->userdata(self::$token_name); } }
Step by step:
- We load the session library, in case it's not being loaded automatically. We need this to store the token.
- We determine if the token was already generated. If the userdata method returns
FALSE
, then the token is not yet present. - We generate a token and store it in the class variable for easy access. The token could be almost anything really. I used those three functions to ensure that it's very random.
- We then store it in the session variable with the name configured previously.
- If the token was already present, we don't generate it and instead store it in the class variable for easy access.
Step 4 - Token Validation
We need to ensure that the token was submitted, and is valid in case the request is a POST
request. Go ahead and paste the following code into your csrf.php
file:
/** * Validates a submitted token when POST request is made. * * @return void * @author Ian Murray */ public function validate_tokens() { // Is this a post request? if ($_SERVER['REQUEST_METHOD'] == 'POST') { // Is the token field set and valid? $posted_token = $this->CI->input->post(self::$token_name); if ($posted_token === FALSE || $posted_token != $this->CI->session->userdata(self::$token_name)) { // Invalid request, send error 400. show_error('Request was invalid. Tokens did not match.', 400); } } }
- Above, we validate the request, but only if it's a
POST
request, which means that a form was, in fact, submitted. We check this by taking a look at theREQUEST_METHOD
within the$_SERVER
super global. - Check if the token was actually posted. If the output of
$this->CI->input->post(self::$token_name)
isFALSE
, then the token was never posted. - If the token wasn't posted or if it's not equal to the one we generated, then deny the request with a "Bad Request" error.
Step 5 - Inject Tokens into the Views
This is the fun part! We need to inject the tokens in all forms. To make life easier for ourselves, we are going to place two meta tags within our <head>
(Rails-like). That way, we can include the token in AJAX requests as well.
Append the following code to your csrf.php
file:
/** * This injects hidden tags on all POST forms with the csrf token. * Also injects meta headers in <head> of output (if exists) for easy access * from JS frameworks. * * @return void * @author Ian Murray */ public function inject_tokens() { $output = $this->CI->output->get_output(); // Inject into form $output = preg_replace('/(<(form|FORM)[^>]*(method|METHOD)="(post|POST)"[^>]*>)/', '$0<input type="hidden" name="' . self::$token_name . '" value="' . self::$token . '">', $output); // Inject into <head> $output = preg_replace('/(<\/head>)/', '<meta name="csrf-name" content="' . self::$token_name . '">' . "\n" . '<meta name="csrf-token" content="' . self::$token . '">' . "\n" . '$0', $output); $this->CI->output->_display($output); }
- Since this is a
display_override
hook, we need to retrieve the generated output from CodeIgniter. We do this by using the$this->CI->output->get_output()
method. - To inject the tags in our forms, we'll use regular expressions. The expression ensures that we inject a hidden input tag (which contains our generated token) only into forms with a method of type
POST
. - We also need to inject our meta tags into the
header
(if present). This is simple, since the closing head tag should only be present once per file. - Lastly, since we're using the
display_override
hook, the default method to display your view will not be called. This method includes all sorts of things, which we should not - just for the purposes of injecting some code. Calling it ourselves solves this.
Step 6 - Hooks
Last, but not least, we need to create the hooks themselves - so our methods get called. Paste the following code into your system/application/config/hooks.php
file:
// // CSRF Protection hooks, don't touch these unless you know what you're // doing. // // THE ORDER OF THESE HOOKS IS EXTREMELY IMPORTANT!! // // THIS HAS TO GO FIRST IN THE post_controller_constructor HOOK LIST. $hook['post_controller_constructor'][] = array( // Mind the "[]", this is not the only post_controller_constructor hook 'class' => 'CSRF_Protection', 'function' => 'validate_tokens', 'filename' => 'csrf.php', 'filepath' => 'hooks' ); // Generates the token (MUST HAPPEN AFTER THE VALIDATION HAS BEEN MADE, BUT BEFORE THE CONTROLLER // IS EXECUTED, OTHERWISE USER HAS NO ACCESS TO A VALID TOKEN FOR CUSTOM FORMS). $hook['post_controller_constructor'][] = array( // Mind the "[]", this is not the only post_controller_constructor hook 'class' => 'CSRF_Protection', 'function' => 'generate_token', 'filename' => 'csrf.php', 'filepath' => 'hooks' ); // This injects tokens on all forms $hook['display_override'] = array( 'class' => 'CSRF_Protection', 'function' => 'inject_tokens', 'filename' => 'csrf.php', 'filepath' => 'hooks' );
- The first hook calls the validate_tokens method. This is not the only
post_controller_constructor
hook, so we need to add those brackets ("[]
"). Refer to the documentation on CodeIgniter hooks for more info. - The second hook, which is also a
post_controller_constructor
, generates the token in case it hasn't been generated yet. - The third one is the display override. This hook will inject our tokens into the
form
and theheader
.
Wrapping Up
With minimal effort, we've built quite a nice library for ourselves.
You can use this library in any project, and it will automagically protect your site against CSRF.
If you'd like to contribute to this little project, please leave a comment below, or fork the project on GitHub. Alternatively, as of CodeIgniter v2.0, protection against CSRF attacks is now built into the framework!
Comments