From c221381c0277abedb8737e4ad1d0de25479af154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Wed, 21 Oct 2015 17:09:31 +0200 Subject: [PATCH 1/3] [FEATURE] csrf token - Removed request-type prefix when loading methods. - Optimised csrf token class. --- src/Pecee/CsrfToken.php | 44 ++++++-------------------- src/Pecee/SimpleRouter/RouterEntry.php | 2 +- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/src/Pecee/CsrfToken.php b/src/Pecee/CsrfToken.php index e3cd7a6..80311c7 100644 --- a/src/Pecee/CsrfToken.php +++ b/src/Pecee/CsrfToken.php @@ -3,30 +3,15 @@ namespace Pecee; class CsrfToken { - const CSRF_KEY = 'csrf_token'; + const CSRF_KEY = 'csrf'; - protected static $instance; - - protected $lastToken; - protected $currentToken; - - public static function getInstance() { - if(self::$instance === null) { - self::$instance = new static(); - } - return self::$instance; - } + protected $token; public function __construct() { $this->lastToken = isset($_SESSION[self::CSRF_KEY]) ? $_SESSION[self::CSRF_KEY] : null; $this->currentToken = $this->generate(); - // Initialise session, if it hasn't been initialised. - if(!isset($_SESSION)) { - session_start(); - } - - $_SESSION['csrf_token'] = $this->currentToken; + $_COOKIE[self::CSRF_KEY] = $this->currentToken; } /** @@ -47,28 +32,17 @@ class CsrfToken { * @return bool */ public function validate($token) { - return hash_equals($token, $_SESSION[self::CSRF_KEY]); + return hash_equals($token, $this->getCurrentToken()); } /** * @return string|null */ - public function getLastToken(){ - return $this->lastToken; - } - - /** - * @param string|null $lastToken - */ - public function setLastToken($lastToken){ - $this->lastToken = $lastToken; - } - - /** - * @return string|null - */ - public function getCurrentToken(){ - return $this->currentToken; + public function getToken(){ + if(isset($_COOKIE[self::CSRF_KEY])) { + return $_COOKIE[self::CSRF_KEY]; + } + return null; } } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterEntry.php b/src/Pecee/SimpleRouter/RouterEntry.php index 37fa567..aa50533 100644 --- a/src/Pecee/SimpleRouter/RouterEntry.php +++ b/src/Pecee/SimpleRouter/RouterEntry.php @@ -269,7 +269,7 @@ abstract class RouterEntry { $className = $this->getNamespace() . '\\' . $controller[0]; $class = $this->loadClass($className); - $method = $request->getMethod() . ucfirst($controller[1]); + $method = $controller[1]; if (!method_exists($class, $method)) { throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); From 1ba05b923ce516fec44f0e5b181a2b0dfa5811d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Wed, 21 Oct 2015 18:12:53 +0200 Subject: [PATCH 2/3] [FEATURE] Csrf token - Added functionality to CsrfToken class. - Added header support to Request class. - Added option to set BaseCsrfVerifier class in RouterBase and SimpleRouter. --- src/Pecee/CsrfToken.php | 26 +++++++++++---- .../Http/Middleware/BaseCsrfVerifier.php | 33 +++++++++++++++++++ src/Pecee/Http/Middleware/Middleware.php | 4 +-- src/Pecee/Http/Middleware/VerifyCsrfToken.php | 9 ----- src/Pecee/Http/Request.php | 19 +++++++++++ src/Pecee/SimpleRouter/RouterBase.php | 33 +++++++++++++++++-- src/Pecee/SimpleRouter/SimpleRouter.php | 15 +++++++++ 7 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 src/Pecee/Http/Middleware/BaseCsrfVerifier.php delete mode 100644 src/Pecee/Http/Middleware/VerifyCsrfToken.php diff --git a/src/Pecee/CsrfToken.php b/src/Pecee/CsrfToken.php index 80311c7..ebf4aa6 100644 --- a/src/Pecee/CsrfToken.php +++ b/src/Pecee/CsrfToken.php @@ -3,22 +3,21 @@ namespace Pecee; class CsrfToken { - const CSRF_KEY = 'csrf'; + const CSRF_KEY = 'XSRF-TOKEN'; protected $token; public function __construct() { - $this->lastToken = isset($_SESSION[self::CSRF_KEY]) ? $_SESSION[self::CSRF_KEY] : null; - $this->currentToken = $this->generate(); - - $_COOKIE[self::CSRF_KEY] = $this->currentToken; + if($this->getToken() === null) { + $this->setToken($this->generateToken()); + } } /** * Generate random identifier for CSRF token * @return string */ - public static function generate() { + public static function generateToken() { if (function_exists('mcrypt_create_iv')) { return bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM)); } @@ -32,10 +31,23 @@ class CsrfToken { * @return bool */ public function validate($token) { - return hash_equals($token, $this->getCurrentToken()); + if($token !== null && $this->getToken() !== null) { + return hash_equals($token, $this->getToken()); + } + return false; } /** + * Set csrf token cookie + * + * @param $token + */ + public function setToken($token) { + setcookie(self::CSRF_KEY, $token, time() + 60 * 120, '/'); + } + + /** + * Get csrf token * @return string|null */ public function getToken(){ diff --git a/src/Pecee/Http/Middleware/BaseCsrfVerifier.php b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php new file mode 100644 index 0000000..4a401cc --- /dev/null +++ b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php @@ -0,0 +1,33 @@ +getMethod() != 'get') { + + $token = (isset($_POST[self::POST_KEY])) ? $_POST[self::POST_KEY] : null; + + // If the token is not posted, check headers for valid x-csrf-token + if($token === null) { + $token = $request->getHeader(self::HEADER_KEY); + } + + $tokenValidator = new CsrfToken(); + if( !$tokenValidator->validate( $token ) ) { + throw new RouterException('Invalid csrf-token.'); + } + + } + + } +} \ No newline at end of file diff --git a/src/Pecee/Http/Middleware/Middleware.php b/src/Pecee/Http/Middleware/Middleware.php index 3f04715..4b70ec0 100644 --- a/src/Pecee/Http/Middleware/Middleware.php +++ b/src/Pecee/Http/Middleware/Middleware.php @@ -7,7 +7,5 @@ use Pecee\SimpleRouter\RouterEntry; abstract class Middleware { - public function handle(Request $request) { - return true; - } + abstract function handle(Request $request); } \ No newline at end of file diff --git a/src/Pecee/Http/Middleware/VerifyCsrfToken.php b/src/Pecee/Http/Middleware/VerifyCsrfToken.php deleted file mode 100644 index fdaa63f..0000000 --- a/src/Pecee/Http/Middleware/VerifyCsrfToken.php +++ /dev/null @@ -1,9 +0,0 @@ -host = $_SERVER['HTTP_HOST']; $this->uri = rtrim($_SERVER['REQUEST_URI'], '/') . '/'; $this->method = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : strtolower($_SERVER['REQUEST_METHOD']); + $this->headers = getallheaders(); } /** @@ -50,4 +52,21 @@ class Request { return (isset($_SERVER['PHP_AUTH_PW'])) ? $_SERVER['PHP_AUTH_PW']: null; } + /** + * Get headers + * @return array + */ + public function getHeaders() { + return $this->headers; + } + + /** + * Get header value by name + * @param string $name + * @return string|null + */ + public function getHeader($name) { + return (isset($this->headers[$name])) ? $this->headers[$name] : null; + } + } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterBase.php b/src/Pecee/SimpleRouter/RouterBase.php index b5cc3d0..3b6559c 100644 --- a/src/Pecee/SimpleRouter/RouterBase.php +++ b/src/Pecee/SimpleRouter/RouterBase.php @@ -2,6 +2,7 @@ namespace Pecee\SimpleRouter; use Pecee\ArrayUtil; +use Pecee\Http\Middleware\BaseCsrfVerifier; use Pecee\Http\Request; use Pecee\Url; @@ -17,6 +18,7 @@ class RouterBase { protected $backstack; protected $loadedRoute; protected $defaultNamespace; + protected $baseCsrfVerifier; // TODO: make interface for controller routers, so they can be easily detected // TODO: clean up - cut some of the methods down to smaller pieces @@ -26,6 +28,7 @@ class RouterBase { $this->backstack = array(); $this->controllerUrlMap = array(); $this->request = new Request(); + $this->baseCsrfVerifier = new BaseCsrfVerifier(); } public function addRoute(RouterEntry $route) { @@ -85,8 +88,16 @@ class RouterBase { } public function routeRequest() { - // Loop through each route-request + // Verify csrf token for request + if($this->baseCsrfVerifier !== null) { + /* @var $csrfVerifier BaseCsrfVerifier */ + $csrfVerifier = $this->baseCsrfVerifier; + $csrfVerifier = new $csrfVerifier(); + $csrfVerifier->handle($this->request); + } + + // Loop through each route-request $this->processRoutes($this->routes); // Make sure the urls is in the right order when comparing @@ -100,7 +111,6 @@ class RouterBase { foreach($this->controllerUrlMap as $route) { $routeMatch = $route->matchRoute($this->request); - if($routeMatch && !($routeMatch instanceof RouterGroup)) { if(count($route->getRequestMethods()) && !in_array($this->request->getMethod(), $route->getRequestMethods())) { @@ -179,6 +189,25 @@ class RouterBase { return $this->request; } + /** + * Get base csrf verifier class + * @return BaseCsrfVerifier + */ + public function getBaseCsrfVerifier() { + return $this->baseCsrfVerifier; + } + + /** + * Set base csrf verifier class + * + * @param BaseCsrfVerifier $baseCsrfVerifier + * @return self + */ + public function setBaseCsrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) { + $this->baseCsrfVerifier = $baseCsrfVerifier; + return $this; + } + protected function processUrl($route, $method = null, $parameters = null, $getParams = null) { $url = $route->getUrl(); diff --git a/src/Pecee/SimpleRouter/SimpleRouter.php b/src/Pecee/SimpleRouter/SimpleRouter.php index 99b050a..c4e6501 100644 --- a/src/Pecee/SimpleRouter/SimpleRouter.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -9,14 +9,29 @@ namespace Pecee\SimpleRouter; +use Pecee\Http\Middleware\BaseCsrfVerifier; + class SimpleRouter { + /** + * Start/route request + * @param null $defaultNamespace + * @throws RouterException + */ public static function start($defaultNamespace = null) { $router = RouterBase::GetInstance(); $router->setDefaultNamespace($defaultNamespace); $router->routeRequest(); } + /** + * Set base csrf verifier + * @param BaseCsrfVerifier $baseCsrfVerifier + */ + public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) { + RouterBase::getInstance()->setBaseCsrfVerifier($baseCsrfVerifier); + } + public static function get($url, $callback, array $settings = null) { $route = new RouterRoute($url, $callback); $route->addSettings($settings); From 9d6a3c328ff29087fe3c669ebbc5d51cc359900e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Wed, 21 Oct 2015 18:30:03 +0200 Subject: [PATCH 3/3] [TASK] Updated documentation. --- README.md | 38 ++++++++++++++++--- .../Http/Middleware/BaseCsrfVerifier.php | 1 + 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cc74051..bf59bf5 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,22 @@ Add the latest version pf Simple PHP Router to your ```composer.json``` ## Notes +### Features + +- Basic routing (get, post, put, delete) with support for custom multiple verbs. +- Regular Expression Constraints for parameters. +- Named routes. +- Generating url to routes. +- Route groups. +- Middleware (classes that intercepts before the route is rendered). +- Namespaces. +- Route prefixes. +- CSRF protection. + ### Features currently "in-the-works" - Global Constraints -- Named Routes - Sub-Domain Routing -- CSRF Protection - Optional/required parameters ## Initialising the router @@ -138,8 +148,11 @@ class Router extends SimpleRouter { // Init locale settings Locale::getInstance(); - // Set default namespace + // Set default namespace for routes $defaultNamespace = '\\'.Registry::getInstance()->get('AppName') . '\\Controller'; + + // Add custom csrf verifier (must extend BaseCsrfVerifier) + parent::csrfVerifier('MyProject\Middleware\CustomCsrfVerifier'); // Handle exceptions try { @@ -171,13 +184,28 @@ function url($controller, $parameters = null, $getParams = null) { } ``` +This is a basic example for getting the current csrf token + +```php +/** + * Get current csrf-token + * @return null|string + */ +function csrf_token() { + $token = new \Pecee\CsrfToken(); + return $token->getToken(); +} +``` + +### Example for getting the url + In ```routes.php``` we have added this route: -```SimpleRouter::get('/item/{id}', 'myController@show');``` +```SimpleRouter::get('/item/{id}', 'myController@show', ['as' => 'item']);``` In the template we then call: -```url('myController@show', ['id' => 22], ['category' => 'shoes']);``` +```url('item', ['id' => 22], ['category' => 'shoes']);``` Result url is: diff --git a/src/Pecee/Http/Middleware/BaseCsrfVerifier.php b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php index 4a401cc..3d8032e 100644 --- a/src/Pecee/Http/Middleware/BaseCsrfVerifier.php +++ b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php @@ -30,4 +30,5 @@ class BaseCsrfVerifier extends Middleware { } } + } \ No newline at end of file