diff --git a/.travis.yml b/.travis.yml index b88ac90..0230d90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,13 @@ language: php php: - 7.0 - 7.1 +- 7.2 - nightly matrix: allow_failures: - php: nightly + - php: 7.2 before_script: - composer install diff --git a/CHANGELOG.md b/CHANGELOG.md index f679653..7dcc63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [v1.0.1](https://github.com/linna/csrf-guard/compare/v1.0.0...v1.0.1) - 2017-XX-XX +## [v1.1.0](https://github.com/linna/csrf-guard/compare/v1.0.0...v1.1.0) - 2017-08-20 + +### Added +* `getTimedToken()` method for expiring tokens + +### Changed +* `validate()` naw can validate for timed tokens +* Tests updated +* Internal methods refactor + +### Deprecated +* `getHiddenInput()` method ### Fixed * `private $session;` docblock diff --git a/src/CsrfGuard.php b/src/CsrfGuard.php index 1e82b6f..784b65f 100644 --- a/src/CsrfGuard.php +++ b/src/CsrfGuard.php @@ -77,30 +77,67 @@ private function dequeue(array &$array) */ public function getToken() : array { - $tokenName = 'csrf_'.bin2hex(random_bytes(8)); - $token = bin2hex(random_bytes($this->tokenStrength)); + $token = $this->generateToken(); - $this->session['CSRF'][$tokenName] = $token; + $name = $token['name']; + + $this->session['CSRF'][$name] = $token; //storage cleaning! //warning!! if you get in a page more token of maximun storage, //will there a leak of token, the firsts generated //in future I think throw and exception. $this->dequeue($this->session['CSRF']); - - return ['name' => $tokenName, 'token' => $token]; + + return $token; } + /** + * Return timed csrf token as array. + * + * @param int $ttl Time to live for the token. + * + * @return array + */ + public function getTimedToken(int $ttl) : array + { + $token = $this->generateToken(); + $token['time'] = time() + $ttl; + + $name = $token['name']; + + $this->session['CSRF'][$name] = $token; + + $this->dequeue($this->session['CSRF']); + + return $token; + } + + /** + * Generate a random token. + * + * @return array + */ + private function generateToken() : array + { + $name = 'csrf_'.bin2hex(random_bytes(8)); + $value = bin2hex(random_bytes($this->tokenStrength)); + + return ['name' => $name, 'value' => $value]; + } + /** * Return csrf token as hidden input form. * * @return string + * + * @deprecated since version 1.1.0 */ public function getHiddenInput() : string { $token = $this->getToken(); - return ''; + return ''; } /** @@ -113,14 +150,40 @@ public function getHiddenInput() : string */ public function validate(array $requestData) : bool { - $arrayToken = $this->session['CSRF']; + //apply matchToken method elements of passed data, + //using this instead of forach for code shortness. + $array = array_filter($requestData, array($this, 'matchToken'), ARRAY_FILTER_USE_BOTH); + + return (bool) count($array); + } + + /** + * Tests for valid token. + * + * @param string $value + * @param string $key + * + * @return bool + */ + private function matchToken(string $value, string $key) : bool + { + $tokens = $this->session['CSRF']; + + //check if token exist + if (!isset($tokens[$key])) { + return false; + } - foreach ($requestData as $key => $value) { - if (isset($arrayToken[$key]) && hash_equals($arrayToken[$key], $value)) { - return true; - } + //check if token is valid + if (!hash_equals($tokens[$key]['value'], $value)) { + return false; } - return false; + //check if token is expired if timed + if (isset($tokens[$key]['time']) && $tokens[$key]['time'] < time()) { + return false; + } + + return true; } } diff --git a/tests/CsrfGuardTest.php b/tests/CsrfGuardTest.php index a132954..8e8105e 100644 --- a/tests/CsrfGuardTest.php +++ b/tests/CsrfGuardTest.php @@ -113,15 +113,42 @@ public function testDequeue(int $sizeLimit) public function testGetToken() { session_start(); - + $csrf = new CsrfGuard(32, 16); - + $token = $csrf->getToken(); + + $key = key($_SESSION['CSRF']); + $value = $_SESSION['CSRF'][$key]['value']; + + $this->assertEquals($key, $token['name']); + $this->assertEquals($value, $token['value']); + + session_destroy(); + } + + /** + * Test get timed token. + * + * @runInSeparateProcess + */ + public function testGetTimedToken() + { + session_start(); + + $csrf = new CsrfGuard(32, 16); + + $token = $csrf->getTimedToken(5); + $tokenTime = time() + 5; $key = key($_SESSION['CSRF']); + $value = $_SESSION['CSRF'][$key]['value']; + $time = $_SESSION['CSRF'][$key]['time']; $this->assertEquals($key, $token['name']); - $this->assertEquals($_SESSION['CSRF'][$key], $token['token']); + $this->assertEquals($value, $token['value']); + $this->assertEquals($time, $token['time']); + $this->assertEquals($tokenTime, $token['time']); session_destroy(); } @@ -140,7 +167,7 @@ public function testGetHiddenInput() $input = $csrf->getHiddenInput(); $key = key($_SESSION['CSRF']); - $token = $_SESSION['CSRF'][$key]; + $token = $_SESSION['CSRF'][$key]['value']; $this->assertEquals('', $input); @@ -160,7 +187,7 @@ public function testValidate() $csrf->getToken(); $key = key($_SESSION['CSRF']); - $token = $_SESSION['CSRF'][$key]; + $token = $_SESSION['CSRF'][$key]['value']; $this->assertEquals(true, $csrf->validate([$key => $token])); $this->assertEquals(false, $csrf->validate(['foo' => $token]));