Protect your website against CSRF attacks

This article is also available in the following languages:
By T0aD
CSRF attacks take advantage of the fact that if an authenticated client opens a page with a link , the browser will treat it as a regular link (normal!) and send over the credentials to the website, thus allowing the action to be performed.
This component's goal is to suppress that risk by protecting your links with a secret.
Everytime an action is authenticated and performed, the component will automatically regenerate a new name and new value for the secret parameter in URL.

Once this component is installed, all you have to do to protect your website is to add the SecureAction component in your controller and feed the property $securedActions with the actions you want to protect.

Controller Class:

<?php 
class UsersController extends AppController
{
  var 
$components = array('SecureAction');
//  var $securedActions = '*'; // protect all actions of the controller
  
var $securedActions = array('remove');

  function 
remove($userID)
  {
     
$this->User->delete($userID);
  }
}
?>

You will still have of course (I don't know how to that nicely in CakePHP) to generate the links of the actions to be protected by using the component method $this->SecureAction->url($originalURL); in your controllers or $html->surl($originalURL); in your views:

Controller Class:

<?php 
...
  function 
anything()
  {
    
$url $this->SecureAction->url('/users/delete/54');
    
$this->set('deleteLink'$url);
  }
...
?>

View Template:


...
<p>Delete this user by clicking on the following link:
<?php echo $html->link('Delete'$html->surl('/users/delete/54')); ?>
</p>
...

Now the component code you will need to save in app/controllers/components/secure_action.php:

Component Class:

<?php 
<?php
/** Sexy rip of: http://bakery.cakephp.org/articles/view/secureget-component */

class SecureActionComponent extends Object
{
    var 
$name 'SecureAction';
  var 
$components = array('Session''Flash');
    var 
$idLength 16;
    var 
$nameLength 4;

    function 
startup(&$controller)
    {
        if (! 
$this->Session->check($this->name.'.name') || ! $this->Session->check($this->name.'.hashKey')) {
            
$this->regenerate();
        }

        
/** Authenticate this action if necessary */
        
$this->__action strtolower($controller->action);
        if (! empty(
$controller->securedActions)) {
            if (
$controller->securedActions == '*' || in_array($this->__action$controller->securedActions)) {
                
/** Auth required */
                
$rv $this->auth($controller->params['url']['url']);
                if (
$rv == false) {
                    
/** Sets a flash message and redirect */
                     
$controller->Flash->add(__("You do not have right to perform the previous action"true));
                    if (
env('HTTP_REFERER') == '') {
                        
$controller->redirect('/');
                    }
                    
$controller->redirect(env('HTTP_REFERER'));
                } else {
                    
/** Access granted, lets regenerate the key */
                    
$this->regenerate();
                }
            }
        }
    }

    function 
regenerate()
    {
        
$this->Session->write($this->name.'.name'$this->_generate($this->nameLength));
        
$this->Session->write($this->name.'.hashKey'$this->_generate());
    }

    
/**
    * Authenticate the given action
    * @returns false on error, true on success
    */
    
function auth($url)
    {
        if (empty(
$url)) {
            return 
false;
        }
        if (
$url[0] != '/') {
            
$url '/'.$url;
        }
        
$url_t explode('/'$url);
        
$key null;
        for (
$i 0; isset($url_t[$i]); $i++) {
            if (! 
strncmp($url_t[$i], $this->Session->read($this->name.'.name').':'$this->nameLength+1)) {
                
$key $url_t[$i];
            }
        }
        if (
$key == null) {
            return 
false;
        }

        
$url str_replace($key''$url); // we remove the key from the URI
        
$lid str_replace('/'''$url); // we remove all slashes
        
        
$key_t explode(':'$key); // we isolate the key from its name

        
$nkey sha1($this->Session->read($this->name.'.hashKey').$lid);
        if (
$nkey == $key_t[1]) {
            return 
true;
        }
        return 
false;
    }

    
/**
    * Generate an url from the full url (/controller/action/param1:value1/etc...)
    */
    
function url($url)
    {
        
$lid str_replace('/'''$url);
//          $lid = explode('/', $url);
//         $lid = implode('', $lid);
        
$key sha1($this->Session->read($this->name.'.hashKey').$lid);
        
$url .= '/'.$this->Session->read($this->name.'.name').':'.$key;
        return 
$url;
    }

    function 
_generate($length null)
    {
        if (! 
is_n($length)) {
            
$length $this->idLength;
        }
        
$chars "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        
$max strlen($chars)-1;
        
$string '';
        for (
$i 0$i $length$i++) {
            
$string .= $chars[mt_rand(0$max)];
        }
        return 
$string;
    }
}

?>

And finally you need to add in your app_helper.php (for simplicity, to allow access from any Helper, like HtmlHelper):

Helper Class:

<?php 
class AppHelper extends Helper {

    
/** Check SecureAction component */
    
function surl($url) {

        
$view =& ClassRegistry::getObject('view');

        
$lid str_replace('/'''$url);
        
$key sha1($view->loaded['session']->read('SecureAction.hashKey').$lid);
        
$url .= '/'.$view->loaded['session']->read('SecureAction.name').':'.$key;

        return 
$url;
    }
}
?>

Here you go, hope this component will be usefull :)

Thanks for http://bakery.cakephp.org/articles/view/secureget-component to give me some usefull code to start working on right away.
And thanks to the users of http://www.lescigales.org/ to let me know about the issue ;)

Comments

  • Posted 12/21/10 09:50:36 PM
    I found myself needing to make a link to a delete action redirect back to the original page, which is an index that makes use of CakeDC's Search plugin and Paginator. This component needed a little update to make it all work happily together.

    First, make a small change in your view where you create the link so that the named parameters of the current page are included in the secure link:


    echo $this->HtmlExt->slink(__('Delete', true), array_merge(array('action'=>'delete', $id), $this->passedArgs) , null, __('Are you sure you want to delete this record?', true)); ?>

    Then in your controller, change the redirect in the secured action to include named parameters:


    $this->redirect(array_merge(array('action'=>'index'), $this->params['named']));

    Now, here's the change needed for the SecureAction component. Add the following function to the SecureActionComponent class:


    function removeParam(&$controller) {
        $key = $this->Session->read($this->name.'.name');
        if (isset($controller->params['named'][$key])) {
            unset($controller->params['named'][$key]);
        }
    }

    Then, add a call to this new function in the startup method just after the authentication check:


    $rv = $this->auth($controller->params['url']['url']);
    $this->removeParam($controller);
    if ($rv == false) {
  • Posted 08/03/10 07:27:06 AM
    This component needs a few updates to work in CakePHP 1.2 and 1.3. In SecureActionComponent:

    Line 5:
    var $components = array('Session', 'Flash'); Replace with:
    var $components = array('Session');
    Line 25:
    $controller->Flash->add(__("You do not have right to perform the previous action", true)); Replace with:
    $this->Session->setFlash(__("You do not have right to perform the previous action", true));
    Line 94:
    if (! is_n($length)) { Replace with:
    if (empty($length)) {
    If you're using CakePHP's HTML helper to create links, the surl function's output can't be directly passed to $html->link. It took a while to sort out this problem. The component expects the token to be created using part of the URL below Cake's webroot. It turns out that using $html->surl and $html->link together with $html->url results in the webroot part of the URL being included in the address, or included more than once in the URL. To get around this, instead of placing the surl function in AppHelper, I created a new HtmlExt helper:

    html_ext.php

    Helper Class:

    <?php 
    class HtmlExtHelper extends HtmlHelper {

        
    /**
         * SecureAction's surl method. Add secure token to a URL.
         * @param string $url  Cake-relative URL
         * @return string      URL with token added
         */
        
    function surl($url) {

            
    $view =& ClassRegistry::getObject('view');

            
    $lid str_replace('/'''$url);
            
    $key sha1($view->loaded['session']->read('SecureAction.hashKey').$lid);
            
    $url .= '/'.$view->loaded['session']->read('SecureAction.name').':'.$key;

            return 
    $url;
        }
        
        
    /**
         * Cake's HtmlHelper link method, modified so that that the URL is given a SecureAction token
         */
        
    function slink($title$url null$htmlAttributes = array(), $confirmMessage false$escapeTitle true) {
            if (
    $url !== null) {
                
    $url $this->url($url);
            } else {
                
    $url $this->url($title);
                
    $title $url;
                
    $escapeTitle false;
            }

            
    // This is the part added for secure url's
            
    $turl substr($urlstrlen($this->webroot));
            
    $url $this->webroot $this->surl($turl);

            if (isset(
    $htmlAttributes['escape']) && $escapeTitle == true) {
                
    $escapeTitle $htmlAttributes['escape'];
            }

            if (
    $escapeTitle === true) {
                
    $title h($title);
            } elseif (
    is_string($escapeTitle)) {
                
    $title htmlentities($titleENT_QUOTES$escapeTitle);
            }

            if (!empty(
    $htmlAttributes['confirm'])) {
                
    $confirmMessage $htmlAttributes['confirm'];
                unset(
    $htmlAttributes['confirm']);
            }
            if (
    $confirmMessage) {
                
    $confirmMessage str_replace("'""\'"$confirmMessage);
                
    $confirmMessage str_replace('"''\"'$confirmMessage);
                
    $htmlAttributes['onclick'] = "return confirm('{$confirmMessage}');";
            } elseif (isset(
    $htmlAttributes['default']) && $htmlAttributes['default'] == false) {
                if (isset(
    $htmlAttributes['onclick'])) {
                    
    $htmlAttributes['onclick'] .= ' event.returnValue = false; return false;';
                } else {
                    
    $htmlAttributes['onclick'] = 'event.returnValue = false; return false;';
                }
                unset(
    $htmlAttributes['default']);
            }
            return 
    $this->output(sprintf($this->tags['link'], $url$this->_parseAttributes($htmlAttributes), $title));
        }
    }
    ?>

    Usage works almost exactly like the normal $html->link method. For example:

    echo $htmlExt->slink(__('Delete', true), array('controller'=>'users', 'action'=>'delete', $user['User']['id']), null, sprintf(__('Are you sure you want to delete # %s?', true), $user['User']['id']));
    I'd like to see this included in CakePHP's core Security component. Ideally, actions specified in the $securedActions array would be automatically appended with a secure token. I don't know if this is possible though, it would probably require some modifications to Router::url.
  • Posted 06/12/08 03:51:21 PM
    This is really only useful if you're not designing your appilcation according to proper HTTP convention; GET requests should not change the server's state (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html), and the core Security component already handles this automatically for forms.
    • Posted 06/22/08 04:27:33 PM
      This is really only useful if you're not designing your appilcation according to proper HTTP convention; GET requests should not change the server's state (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html), and the core Security component already handles this automatically for forms.
      I totally agree with that, but the reality is quite different, hence this component. More and more people are using GET requests to manipulate data as a shortcut (I cannot blame neither them nor me, since it's the power the tools of the web give to us).
      Standards are good, but sticking to reality in this world is even better !

Comments are closed for articles over a year old