prefix = rtrim($prefix, '_'); $this->storage = &$storage; $this->validateStorage(); $this->storageLimit = $storageLimit; } /** * @return string */ public function getToken() { if (!$this->keyPair && (!$this->isPersistent() || !$this->loadLastToken())) { $this->generateToken(); } return $this->keyPair; } /** * Generates a new CSRF token. * * @return array */ protected function generateToken() { // Generate new CSRF token $name = uniqid($this->prefix); $value = $this->createToken(); $this->saveToStorage($name, $value); $this->keyPair = [ $this->prefix.'_name' => $name, $this->prefix.'_value' => $value, ]; return $this->keyPair; } public function validate() { $result = true; $this->validateStorage(); if (in_array($_SERVER['REQUEST_METHOD'], ['POST'])) { $name = isset($_POST[$this->prefix.'_name']) ? $_POST[$this->prefix.'_name'] : false; $value = isset($_POST[$this->prefix.'_value']) ? $_POST[$this->prefix.'_value'] : false; if (!$name || !$value || !$this->validateToken($name, $value)) { // Need to regenerate a new token, as the validateToken removed the current one. $this->generateToken(); $result = false; } } // Enforce the storage limit $this->enforceStorageLimit(); return $result; } /** * @param $prefix * @param $storage * * @return mixed */ protected function validateStorage() { if (is_array($this->storage)) { return $this->storage; } if ($this->storage instanceof ArrayAccess) { return $this->storage; } if (!array_key_exists($this->prefix, $_SESSION)) { $_SESSION[$this->prefix] = []; } $this->storage = &$_SESSION[$this->prefix]; return $this->storage; } /** * Validate CSRF token from current request against token value stored in $_SESSION. * * @param string $name CSRF name * @param string $value CSRF token value * * @return bool */ protected function validateToken($name, $value) { $token = $this->getFromStorage($name); if (function_exists('hash_equals')) { $result = ($token !== false && hash_equals($token, $value)); } else { $result = ($token !== false && $token === $value); } // If we're not in persistent token mode, delete the token. if (!$this->isPersistent() || !$result) { $this->removeFromStorage($name); } return $result; } /** * Create CSRF token value. * * @return string */ protected function createToken() { return bin2hex(random_bytes($this->strength)); } /** * Save token to storage. * * @param string $name CSRF token name * @param string $value CSRF token value */ protected function saveToStorage($name, $value) { $this->storage[$name] = $value; } /** * Get token from storage. * * @param string $name CSRF token name * * @return string|bool CSRF token value or `false` if not present */ protected function getFromStorage($name) { return isset($this->storage[$name]) ? $this->storage[$name] : false; } /** * Get the most recent key pair from storage. * * @return string[]|null Array containing name and value if found, null otherwise */ protected function loadLastToken() { // Use count, since empty ArrayAccess objects can still return false for `empty` if (count($this->storage) < 1) { return null; } foreach ($this->storage as $name => $value) { continue; } $keyPair = [ $this->prefix.'_name' => $name, $this->prefix.'_value' => $value, ]; if ($keyPair) { $this->keyPair = $keyPair; return true; } return false; } /** * Remove token from storage. * * @param string $name CSRF token name */ protected function removeFromStorage($name) { $this->storage[$name] = ' '; unset($this->storage[$name]); } /** * Remove the oldest tokens from the storage array so that there * are never more than storageLimit tokens in the array. * * This is required as a token is generated every request and so * most will never be used. */ protected function enforceStorageLimit() { if ($this->storageLimit < 1) { return; } // $storage must be an array or implement Countable and Traversable if (!is_array($this->storage) && !($this->storage instanceof Countable && $this->storage instanceof Traversable) ) { return; } if (is_array($this->storage)) { while (count($this->storage) > $this->storageLimit) { array_shift($this->storage); } } else { // array_shift() doesn't work for ArrayAccess, so we need an iterator in order to use rewind() // and key(), so that we can then unset $iterator = $this->storage; if ($this->storage instanceof \IteratorAggregate) { $iterator = $this->storage->getIterator(); } while (count($this->storage) > $this->storageLimit) { $iterator->rewind(); unset($this->storage[$iterator->key()]); } } } /** * Setter for storageLimit. * * @param int $storageLimit Value to set * * @return $this */ public function setStorageLimit($storageLimit) { $this->storageLimit = (int) $storageLimit; } /** * Getter for persistentTokenMode. * * @return bool */ public function isPersistent() { return $this->storageLimit < 0; } }