From 35e3ba19747414d697ede4006655ff69cc35b125 Mon Sep 17 00:00:00 2001 From: Brooke Bryan Date: Sun, 17 May 2020 00:28:33 +0100 Subject: [PATCH] Faster CSRF & CSRF Expiry --- src/Csrf/CsrfDataHandler.php | 17 ++++++++++++----- src/Csrf/CsrfForm.php | 5 ++++- src/Csrf/CsrfValidator.php | 15 +++++++++++---- tests/Csrf/CsrfFormTest.php | 5 +++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Csrf/CsrfDataHandler.php b/src/Csrf/CsrfDataHandler.php index c99c262..12c3869 100644 --- a/src/Csrf/CsrfDataHandler.php +++ b/src/Csrf/CsrfDataHandler.php @@ -4,8 +4,6 @@ use Packaged\Form\DataHandlers\AbstractDataHandler; use Packaged\Form\Decorators\HiddenInputDecorator; use Packaged\Form\Decorators\Interfaces\DataHandlerDecorator; -use function password_hash; -use const PASSWORD_DEFAULT; class CsrfDataHandler extends AbstractDataHandler { @@ -14,10 +12,14 @@ class CsrfDataHandler extends AbstractDataHandler protected $_formSecret; protected $_value; - public function __construct($formSecret, $sessionSecret) + //Total number of minutes this csrf will be valid for, null for unlimited time + protected $_expiryMins; + + public function __construct($formSecret, $sessionSecret, ?int $expiryMinutes) { $this->setFormSecret($formSecret); $this->setSessionSecret($sessionSecret); + $this->_expiryMins = $expiryMinutes; } public function applyNewToken() @@ -30,11 +32,16 @@ public function getDefaultValue() { if($this->_defaultValue === null) { - $this->_defaultValue = password_hash($this->_generatePassword(), PASSWORD_DEFAULT); + $this->_defaultValue = static::generateHash($this->_generatePassword(), $this->_expiryMins === null ? 0 : time()); } return $this->_defaultValue; } + public static function generateHash(string $password, $timestamp = 0): string + { + return md5($password . $timestamp) . base_convert($timestamp, 10, 36); + } + protected function _generatePassword() { return $this->_getFormSecret() . $this->_getSessionSecret(); @@ -80,7 +87,7 @@ public function setSessionSecret($sessionSecret) protected function _setupValidator() { - $this->addValidator(new CsrfValidator($this->_generatePassword())); + $this->addValidator(new CsrfValidator($this->_generatePassword(), $this->_expiryMins)); } protected function _defaultDecorator(): DataHandlerDecorator diff --git a/src/Csrf/CsrfForm.php b/src/Csrf/CsrfForm.php index 941c045..017490e 100644 --- a/src/Csrf/CsrfForm.php +++ b/src/Csrf/CsrfForm.php @@ -13,6 +13,9 @@ class CsrfForm extends Form protected $_sessionSecret; + //Number of minutes the csrf token should be valid for. Setting to null for unlimited time + protected $_tokenExpiryMinutes = 60; + public function __construct($sessionSecret) { $this->_sessionSecret = $sessionSecret; @@ -21,7 +24,7 @@ public function __construct($sessionSecret) protected function _initDataHandlers() { - $this->csrfToken = new CsrfDataHandler($this->_getCsrfSecret(), $this->_sessionSecret); + $this->csrfToken = new CsrfDataHandler($this->_getCsrfSecret(), $this->_sessionSecret, $this->_tokenExpiryMinutes); } protected function _getCsrfSecret() diff --git a/src/Csrf/CsrfValidator.php b/src/Csrf/CsrfValidator.php index e1bbc38..7bdd3f6 100644 --- a/src/Csrf/CsrfValidator.php +++ b/src/Csrf/CsrfValidator.php @@ -4,22 +4,29 @@ use Generator; use Packaged\Validate\AbstractValidator; use Packaged\Validate\ValidationException; -use function password_verify; class CsrfValidator extends AbstractValidator { protected $_password; + protected $_expiryMinutes; - public function __construct(string $password) + public function __construct(string $password, ?int $expiryMinutes) { $this->_password = $password; + $this->_expiryMinutes = $expiryMinutes; } protected function _doValidate($value): Generator { - if(!password_verify($this->_password, $value)) + $timestamp = base_convert(substr($value, 32), 36, 10); + if($this->_expiryMinutes !== null && $timestamp < time() - ($this->_expiryMinutes * 60)) { - yield new ValidationException('invalid or missing CSRF token'); + yield new ValidationException('Anti-Forgery token expired'); + } + + if(CsrfDataHandler::generateHash($this->_password, $timestamp) !== $value) + { + yield new ValidationException('Anti-Forgery token missing or invalid'); } } } diff --git a/tests/Csrf/CsrfFormTest.php b/tests/Csrf/CsrfFormTest.php index 823234a..7776cff 100644 --- a/tests/Csrf/CsrfFormTest.php +++ b/tests/Csrf/CsrfFormTest.php @@ -13,6 +13,11 @@ public function testCsrfToken() $form->csrfToken->applyNewToken(); $formValue = $form->csrfToken->getValue(); + $this->assertTrue($form->csrfToken->isValid()); + $this->assertTrue($form->csrfToken->isValidValue($formValue)); + + $form->csrfToken->applyNewToken(); + $formValue = $form->csrfToken->getValue(); $this->assertTrue($form->csrfToken->isValid()); $this->assertTrue($form->csrfToken->isValidValue($formValue)); $form->csrfToken = $formValue;