1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
<?php
namespace Riimu\Kit\CSRF;
use Riimu\Kit\SecureRandom\SecureRandom;
/**
* Secure CSRF token validator and generator.
*
* CSRFHandler provides a simple way to generate and validate CSRF tokens.
* Precautions have been taken to avoid timing and BREACH attacks. The tokens
* are generated using the SecureRandom library in order to generate secure
* random byte sequences.
*
* @author Riikka Kalliomäki <riikka.kalliomaki@gmail.com>
* @copyright Copyright (c) 2014, Riikka Kalliomäki
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class CSRFHandler
{
/** @var int Number of bytes used in the CSRF token */
const TOKEN_LENGTH = 32;
/** @var string[] List of request methods to validate for the CSRF token */
protected $validatedMethods = ['POST', 'PUT', 'DELETE'];
/** @var SecureRandom Secure random generator for generating secure random bytes */
private $generator;
/** @var Storage\TokenStorage Persistent storage used to store the actual token */
private $storage;
/** @var Source\TokenSource[] Possible sources for submitted tokens */
private $sources;
/** @var string The current actual CSRF token */
private $token;
/** @var callable Callback used to compare strings in constant time */
private $compare;
/**
* Creates a new instance of CSRFHandler.
*
* When creating a new instance, it will be initialized with either cookie
* storage or session storage depending on whether you pass true or false
* as the constructor parameter (defaults to cookie). The actual token won't
* be loaded until the token is validated, though. By default, the handler
* will also use post and header data to look for submitted tokens.
*
* @param bool $useCookies True for cookie storage, false for session storage
*/
public function __construct($useCookies = true)
{
$this->storage = $useCookies ? new Storage\CookieStorage() : new Storage\SessionStorage();
$this->sources = [
new Source\PostSource(),
new Source\HeaderSource(),
];
$this->compare = version_compare(PHP_VERSION, '5.6', '>=')
? 'hash_equals' : [$this, 'compareStrings'];
}
/**
* Sets the random generator for generating secure random bytes.
* @param SecureRandom $generator Secure random generator
*/
public function setGenerator(SecureRandom $generator)
{
$this->generator = $generator;
}
/**
* Returns the current secure random generator.
* @return SecureRandom Current secure random generator
*/
public function getGenerator()
{
if (!isset($this->generator)) {
$this->generator = new SecureRandom();
}
return $this->generator;
}
/**
* Sets the persistent storage for the CSRF token.
*
* The token storage should be set before you create new tokens or attempt
* to validate tokens, because the storage is only used the first time the
* token is needed.
*
* @param Storage\TokenStorage $storage Persistent storage handler
*/
public function setStorage(Storage\TokenStorage $storage)
{
$this->storage = $storage;
}
/**
* Sets the possible sources for submitted token.
*
* Multiple sources can be added using an array. The handler will look for
* the token from the sources in the order they appear in the array.
*
* @param Source\TokenSource[] $sources List of token sources.
*/
public function setSources(array $sources)
{
$this->sources = array_map(function (Source\TokenSource $source) {
return $source;
}, $sources);
}
/**
* Tells if the request method indicates that the CSRF token should be validated.
* @return bool True if the token should be validated, false if not
*/
public function isValidatedRequest()
{
return in_array($_SERVER['REQUEST_METHOD'], $this->validatedMethods, true);
}
/**
* Validates the csrf token in the HTTP request.
*
* This method should be called in the beginning of the request. By default,
* POST, PUT and DELETE requests will be validated for a valid CSRF token.
* If the request does not provide a valid CSRF token, this method will
* kill the script and send a HTTP 400 (bad request) response to the
* browser.
*
* This method also accepts a single parameter than can be either true or
* false. If the parameter is set to true, this method will throw an
* InvalidCSRFTokenException instead of killing the script if no valid CSRF
* token was provided in the request.
*
* This method will always trigger the token storage. If you are using the
* cookie storage, this method must be called before the headers have been
* sent. If you are using the session storage instead, you must start the
* session before calling this method.
*
* @param bool $throw True to throw an exception on invalid token, false to kill the script
* @return bool This method always returns true
* @throws InvalidCSRFTokenException If throwing is enabled and there is no valid csrf token
* @throws Storage\TokenStorageException If the secret token cannot be loaded or stored
*/
public function validateRequest($throw = false)
{
// Ensure that the actual token is generated and stored
$this->getTrueToken();
if (!$this->isValidatedRequest()) {
return true;
}
if (!$this->validateRequestToken()) {
if ($throw) {
throw new InvalidCSRFTokenException('Invalid CSRF token');
}
$this->killScript();
}
return true;
}
/**
* Kills the script execution and sends the appropriate header.
* @codeCoverageIgnore
*/
protected function killScript()
{
header('HTTP/1.0 400 Bad Request');
exit();
}
/**
* Validates the token sent in the request.
* @return bool True if the token sent in the request is valid, false if not
* @throws Storage\TokenStorageException If the secret token cannot be loaded or stored
*/
public function validateRequestToken()
{
$token = $this->getRequestToken();
return is_string($token) && $this->validateToken($token);
}
/**
* Validates the csrf token.
*
* The token must be provided as a base64 encoded string which also includes
* the token encryption key. In other words, you should pass this method the
* exact same string that has been returned by the `getToken()` method.
*
* @param string $token The base64 encoded token provided by getToken()
* @return bool True if the token is valid, false if it is not
* @throws Storage\TokenStorageException If the secret token cannot be loaded or stored
*/
public function validateToken($token)
{
if (!is_string($token)) {
return false;
}
$token = base64_decode($token, true);
if (strlen($token) !== self::TOKEN_LENGTH * 2) {
return false;
}
list($key, $encrypted) = str_split($token, self::TOKEN_LENGTH);
return call_user_func(
$this->compare,
$this->encryptToken($this->getTrueToken(), $key),
$encrypted
);
}
/**
* Generates an encrypted token using a one way hashing algorithm.
* @param string $token The actual token
* @param string $key The randomly generated key
* @return string An encrypted token
*/
private function encryptToken($token, $key)
{
return hash_hmac('sha256', $key, $token, true);
}
/**
* Generates a new secure base64 encoded csrf token.
*
* This method returns a new string every time it is called, because it
* always generates a new encryption key for the token. Of course, each of
* these tokens is a valid CSRF token, unless the `regenerateToken()` method
* is called.
*
* @return string Base64 encoded CSRF token
* @throws Storage\TokenStorageException If the secret token cannot be loaded or stored
*/
public function getToken()
{
$key = $this->getGenerator()->getBytes(self::TOKEN_LENGTH);
return base64_encode($key . $this->encryptToken($this->getTrueToken(), $key));
}
/**
* Regenerates the actual CSRF token.
*
* After this method has been called, any token that has been previously
* generated by `getToken()` is no longer considered valid. It is highly
* recommended to regenerate the CSRF token after any user authentication.
*
* @return CSRFHandler Returns self for call chaining
* @throws Storage\TokenStorageException If the secret token cannot be stored
*/
public function regenerateToken()
{
do {
$token = $this->getGenerator()->getBytes(self::TOKEN_LENGTH);
} while ($token === $this->token);
$this->token = $token;
$this->storage->storeToken($this->token);
return $this;
}
/**
* Returns the current actual CSRF token.
*
* This returns the current actual 32 byte random string that is used to
* validate the CSRF tokens submitted in requests.
*
* @return string The current actual CSRF token
* @throws Storage\TokenStorageException If the secret token cannot be loaded or stored
*/
public function getTrueToken()
{
if (!isset($this->token)) {
$token = $this->storage->getStoredToken();
$this->token = is_string($token) ? $token : '';
}
if (strlen($this->token) !== self::TOKEN_LENGTH) {
$this->regenerateToken();
}
return $this->token;
}
/**
* Returns the token sent in the request.
* @return string|false The token sent in the request or false if there is none
*/
public function getRequestToken()
{
$token = false;
foreach ($this->sources as $source) {
if (($token = $source->getRequestToken()) !== false) {
break;
}
}
return $token;
}
/**
* Compares two string in constant time.
* @param string $knownString String known to be correct by the system
* @param string $userString String submitted by the user for comparison
* @return bool True if the strings are equal, false if not
*/
private function compareStrings($knownString, $userString)
{
$result = "\x00";
for ($i = 0, $length = strlen($knownString); $i < $length; $i++) {
$result |= $knownString[$i] ^ $userString[$i];
}
return $result === "\x00";
}
}