* * @license http://opensource.org/licenses/bsd-license.php BSD * * @version $Id$ * */ class Solar_Csrf extends Solar_Base { /** * * The current value of the anti-CSRF token. * * @var string * */ static protected $_current = null; /** * * The name of the $_POST key containing the anti-CSRF token. * * @var string * */ static protected $_key = '__csrf_key'; /** * * A Solar_Request dependency. * * @var Solar_Request * */ static protected $_request; /** * * A Solar_Session object. * * @var Solar_Session * */ static protected $_session; /** * * Has the token value been updated? * * @var bool * */ static protected $_updated = false; /** * * Post-construction tasks to complete object construction. * * @return void * */ protected function _postConstruct() { if (! self::$_session) { self::$_session = Solar::factory('Solar_Session', array( 'class' => 'Solar_Csrf', )); } if (! self::$_request) { self::$_request = Solar_Registry::get('request'); } // ignore construct-time configuration for the key, but honor // it from the config file. we want the key name to be the // same everywhere all the time. $key = Solar_Config::get('Solar_Csrf', 'key'); if ($key) { self::$_key = $key; } } /** * * Updates this object with current values. * * This helps to maintain transitions between not having a session and * then having one; in the non-session state, there will be no token, * so we can't expect its presence until the next page load. * * @return void * */ protected function _update() { if (self::$_updated) { // already updated with current values return; } // lazy-start the session if one exists self::$_session->lazyStart(); if (! self::$_session->isStarted()) { // not started, nothing left to do return; } // the session has started. is there an existing csrf token? if (self::$_session->has('token')) { // retain the existing token self::$_current = self::$_session->get('token'); } else { // no token, create a new one for the session. // we're transitioning from a non-token state, and // incoming forms won't have it yet, so we don't retain // the new token as the current value. self::$_session->set('token', uniqid(mt_rand(), true)); } self::$_updated = true; } /** * * Returns the name of the token key in $_POST values. * * @return string * */ public function getKey() { return self::$_key; } /** * * Gets the token value to be used in outgoing forms. * * @return string * */ public function getToken() { $this->_update(); return self::$_session->get('token'); } /** * * Sets the token value to be used in outgoing forms. * * @param string $token The new token value. * * @return string * */ public function setToken($token) { $this->_update(); self::$_session->set('token', $token); } /** * * Is there a token value in the session already? * * @return string * */ public function hasToken() { $this->_update(); return self::$_session->has('token'); } /** * * Returns the expected incoming value for the token. * * Note that this may be different from the outgoing value, especially in * transitions from not having a session to having one. * * @return string * */ public function getCurrent() { $this->_update(); return self::$_current; } /** * * Does the incoming request look like a cross-site forgery? * * Only works for POST requests. * * @return string * */ public function isForgery() { $this->_update(); if (! self::$_request->isPost()) { // only POST requests can be cross-site request forgeries return false; } if (! self::$_current) { // there is no current value so it doesn't matter return false; } // get the incoming csrf value from $_POST $key = $this->getKey(); $val = self::$_request->post($key); // if they don't match, it's a forgery return $val != self::$_current; } }