Zend Framework CSFR plugin protection

L’idea è semplice, scrivere un plugin che funzioni in Zend Framework per proteggere le form da CSFR. La tecnica è quella di sempre: tracciare la richiesta affinché si abbia la certezza che questa provenga da una sessione valida, attiva e dallo stesso utente che ha legittimamente aperto la sessione.

Per far ciò esiste già un componente compreso in Zend Framework: Zend_Form_Element_Hash. Funziona ottimamente, ma volevo qualcosa di un po’ più strong e che funzionasse out of the box.

Il funzionamento prevede l’aggiunta nella form di due campi type="hidden" contenenti un valore costituito da un hash casuale. I due token così determinati sono interdipendenti perché rappresentano la coppia chiave valore di un oggetto contenuto in un namespace di sessione che può essere diverso da quello usato dall’applicazione. Una volta validata la richiesta, la coppia muore.

Inoltre un timout controlla e ripulisce i token inutilizzati ed ancora vivi in sessione, impedendone un uso nel futuro. Ovviamente questo complica un po’ le cose, perché un utente che sta su una form molto tempo, al momento del submit potrebbe ritrovarsi con il token scaduto, ma questo sarà a discrezione del programmatore, visto che il timeout può essere definito per ogni form.

<?php
class App_Controller_Plugin_Token extends Zend_Form_Decorator_Abstract
{
    protected $_format = '<input type="hidden" name="%s" id="%s" value="%s" />';
    protected $_namespace = 'Tokenize';
    protected $_timeout = 3600;
    protected $_id = 'token';
    protected $_session;
    protected $_request;
    protected $_redirector;
   
    public function __construct($namespace=null, $timeout=null, $id=null, $redirector=null){
        if($namespace and $namespace != ''){
            $this->_namespace = $namespace;
        }
       
        if($timeout and $timeout != ''){
            $this->_timeout = $timeout;
        }
       
        if($id and $id != ''){
            $this->_id = $id;
        }
       
        if($redirector and $redirector != ''){
            $this->_redirector = $redirector;
        }
       
        $this->_session = new Zend_Session_Namespace($this->_namespace, true);
       
        $this->destroyExpired();
       
        $this->_request = new Zend_Controller_Request_Http;
       
        if($this->_request->isPost()){
            $data = $this->_request->getPost();
            $this->validation($data[$this->_id.'k'], $data[$this->_id.'v']);
        }elseif($this->_request->isGet()){
            $data = $this->_request->getParams();
            if(isset($data[$this->_id.'k']) && isset($data[$this->_id.'v'])){
                $this->validation($data[$this->_id.'k'], $data[$this->_id.'v']);
            }
        }
       
    }
   
    public function render($content){
   
        $element  = $this->getElement();
        $id       = $this->_id;
        $name1    = $this->_id.'k';
        $id1      = $this->_id.'k';
        $value1   = $this->createToken();
        $expire   = (time()+$this->_timeout);
        $name2    = $this->_id.'v';
        $id2      = $this->_id.'v';
        $value2   = $this->createToken();
       
        $markup   = sprintf($this->_format, $name1, $id1, $value1);
        $markup     .= sprintf($this->_format, $name2, $id2, $value2);
       
        $this->_session->token[$value1] = $value2;
        $this->_session->expiretoken[$value1] = $expire;
       
        return $markup;
    }
   
    protected function createToken(){
        return md5(time().'Just a random string'.rand());
    }
   
    protected function destroyExpired(){
        if($this->_session->expiretoken){
            foreach($this->_session->expiretoken as $keytoken => $timeout){
                if($timeout < time()){
                    unset($this->_session->token[$keytoken]);
                    unset($this->_session->expiretoken[$keytoken]);
                }
            }
        }
    }
   
    protected function dispatchRedirect(){
        if($this->_redirector){
            return Zend_Controller_Front::getInstance()->getResponse()->setRedirect($this->_redirector);
        }else{
            throw new Zend_Form_Decorator_Exception('Invalid Token');
        }
    }
   
    protected function validation($keytoken, $keyvalue){
        if(isset($this->_session->token[$keytoken])){
            if($this->_session->token[$keytoken] == $keyvalue){
                unset($this->_session->token[$keytoken]);
                unset($this->_session->expiretoken[$keytoken]);
                return true;
            }
        }
        return $this->dispatchRedirect();
    }
}

A discrezione dello sviluppatore, supportati da valori di default in caso di lazy programming, la scelta dei seguenti elementi:

  • $_namespace: il namespace di sessione che conterrà il / i token autorizzativi.
  • $_timeout: il timeout di validità di ogni token.
  • $_id: il prefisso usato come nome / id dei campi che conterranno i token.
  • $_redirector: indirizzo a cui ridirezionare le richieste con token invalido. In caso sia null viene fatta scattare un’eccezione.

L’uso nella form è semplice.

<?php

class App_Form_Login extends Zend_Form
{   
    public function __construct($options=null){
        parent::__construct($options);
       
        $this->setName('Login')
             ->setAction('/login')
             ->setMethod('post');
                           
        $username = new Zend_Form_Element_Text('username');
        $username->setRequired(true)
             ->setLabel('Username')
             ->addFilter('StripTags')
                        ->addFilter('StringTrim')
                        ->addFilter('StringToLower')
                        ->addValidator('Regex', false, array('/^[a-z0-9_]{6,}$/i'));
                   
            $password = new Zend_Form_Element_Password('password');
            $password->setRequired(true)
                 ->setLabel('Password')
                 ->addFilter('StringTrim');
                   
        $submit = new Zend_Form_Element_Submit('login');
        $submit->setLabel('Login');
   
            $tokenize = new App_Controller_Plugin_Token('','', '', '/invalidtoken');
            $token = new Zend_Form_Element('MyToken');
            $token->addDecorator($token);

                 
        $this->addElements(array($username, $password, $submit, $token));
    }   
}

Il codice è rilasciato sotto licenza GNU/LGPL 3.0. Siete liberi di farne più o meno quello che volete, ma soprattutto di migliorarlo e condividere le vostre modifiche.