We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

CSRF security

Hello.

I am working on an ajax based site, that uses the CSRF in every form.

the problem is, that whenever a user opens a new tab, with a form, and goes back to the other tab again, the token has updated and the user will be getting the message "This token is not valid" if he/she submits the form.

How can i prevent this? Do I need one token for the entire session and not genereate new for every page with form?



16.3k

I am not an expert on CSRF but I have used one token per form. I don't generate tokens for each tab of a form - all the tabs of a form get submitted once only. I have used the approach mentioned here to make it generic - Is beforeExecuteRoute a good place to put CSRF checking?.

See Phalcon\Security::checkToken

The last params allow to not destroy the token a each check.

Try :

$security->checkToken(
    'your-token-key', 
    $request->getHeader('X_CSRF_TOKEN'), 
    false // do not destroy the token
);

With this, the token will remain the same, as long as the session will be valid.

I wrote my own CSRF component that generates the token once per session. The token is generated the first time the component is instantiated.

As for checking CSRF, I have that in beforeDispatch rather than beforeExecuteRoute. The rationale for that, is beforeDispatch is executed earlier, so less work is done on behalf of invalid requests.



16.3k

Can you possibly share some code @quasipickle - component & beforeDispatch. TIA.

Component:

<?PHP
namespace Component;

/**
 * This component handles all Cross-Site Request Forgery (CSRF) operations
 * 
 * This class behaves very similar to the built-in-Phalcon Security component.
 * The difference is that this component only generates the token & key once per session,
 * thereby allowing it to be invoked multiple times in a single page, and be used on multiple,
 * concurrent pages.
 * 
 */
class CSRF extends \Phalcon\Mvc\User\Component{

    /**
     * Make sure any POST requests contain a valid CSRF key & token
     * 
     * This method called by the Dispatcher's Event manager because this component 
     * was added as a dispatch listener in bootstrap.php.
     *
     * Forwards (not redirect) to index/csrf if the token wasn't set.
     * 
     * @param  $Event The Event causing the method to be triggered
     * @param  $Dispatcher The Event Dispatcher
     */
    public function beforeDispatch(\Phalcon\Events\Event $Event,\Phalcon\Mvc\Dispatcher $Dispatcher){
        # Handle CSRF check
        if($Dispatcher->getControllerName() != 'index' && $Dispatcher->getActionName() != 'csrf'){
            if($this->request->isPost()){
                if(!$this->checkToken()){
                    if($this->request->isAjax()){
                        echo 'The submitted information did not include a CSRF token, which is required to ensure you actually meant to submit the form.';
                        exit();
                    }
                    else{
                        $Dispatcher->forward(['controller'=>'index','action'=>'csrf']);
                    }
                }
            }
        }
    }

    /**
     * Get the CSRF token key
     * 
     * Generates the key & token if key wasn't already set
     * 
     * @see  self::generateToken()
     * 
     * @return string The token key
     */
    public function getTokenKey(): string {
        if(!$this->session->get('csrf_token_key'))
            $this->generateToken();

        return $this->session->get('csrf_token_key');
    }

    /**
     * Get the CSRF token
     * 
     * Generates the key & token if token wasn't already set
     * 
     * @see  self::generateToken()
     * 
     * @return string The token
     */
    public function getToken(): string {
        if(!$this->session->get('csrf_token'))
            $this->generateToken();

        return $this->session->get('csrf_token');
    }

    /**
     * Checks $_POST to ensure the proper token key & token were POSTed
     * 
     * @return boolean whether or not the appropriate values were found it $_POST
     */
    public function checkToken(): bool{
        $stored_key   = $this->getTokenKey();
        $stored_token = $this->getToken();
        $passed_token = $this->request->getPost($stored_key);

        if($stored_token == $passed_token)
            return TRUE;
        else
            return FALSE;
    }

    /**
     * Generates the token & key and stores them in session
     */
    private function generateToken(){
        $this->session->set('csrf_token_key',$this->security->getTokenKey());
        $this->session->set('csrf_token',$this->security->getToken());
    }
}

Then, in my bootstrap.php file, I add the component as an event listener:

$DI->set('dispatcher',function(){
        // Create an events manager that checks authentication,
        // and propagates user information before dispatching
        $EM = new \Phalcon\Events\Manager();

        // Check any POSTed form had CSRF properly set
        $EM->attach('dispatch:beforeDispatch',new \Component\CSRF());

        $Dispatcher = new \Phalcon\Mvc\Dispatcher();
        $Dispatcher->setEventsManager($EM);

        return $Dispatcher;
    });