Skip to content

Commit

Permalink
Started on turning the CSRF component into a middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
burzum committed Apr 19, 2017
1 parent 60142f3 commit fe1c643
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 0 deletions.
177 changes: 177 additions & 0 deletions src/Http/Middleware/CsrfProtectionMiddleware.php
@@ -0,0 +1,177 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http\Middleware;

use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\I18n\Time;
use Cake\Network\Exception\InvalidCsrfTokenException;
use Cake\Utility\Security;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* Provides CSRF protection & validation.
*
* This component adds a CSRF token to a cookie. The cookie value is compared to
* request data, or the X-CSRF-Token header on each PATCH, POST,
* PUT, or DELETE request.
*
* If the request data is missing or does not match the cookie data,
* an InvalidCsrfTokenException will be raised.
*
* This component integrates with the FormHelper automatically and when
* used together your forms will have CSRF tokens automatically added
* when `$this->Form->create(...)` is used in a view.
*/
class CsrfProtectionMiddleware
{
/**
* Default config for the CSRF handling.
*
* - cookieName = The name of the cookie to send.
* - expiry = How long the CSRF token should last. Defaults to browser session.
* - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false.
* - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false.
* - field = The form field to check. Changing this will also require configuring
* FormHelper.
*
* @var array
*/
protected $_defaultConfig = [
'cookieName' => 'csrfToken',
'expiry' => 0,
'secure' => false,
'httpOnly' => false,
'field' => '_csrfToken',
];

/**
* Configuration
*
* @var array
*/
protected $_config = [];

/**
* Constructor
*
* @param array $config Config options
*/
public function __construct(array $config = [])
{
$this->_config = $this->_defaultConfig + $config;
}

/**
* Serve assets if the path matches one.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Psr\Http\Message\ResponseInterface $response The response.
* @param callable $next Callback to invoke the next middleware.
* @return \Psr\Http\Message\ResponseInterface A response
*/
public function __invoke(ServerRequestInterface &$request, ResponseInterface $response, $next)
{
$cookies = $request->getCookieParams();
$cookieData = null;
if (isset($cookies[$this->_config['cookieName']])) {
$cookieData = isset($cookies[$this->_config['cookieName']]);
}

if (!empty($cookieData)) {
$params = $request->getAttribute('params');
$params['_csrfToken'] = $cookieData;
$request = $request->withAttribute('params', $params);
}

$method = $request->getMethod();
if ($method === 'requested') {
return $next($request, $response);
}

if ($method === 'GET' && $cookieData === null) {
$this->_setCookie($request, $response);

return $next($request, $response);
}

$this->_validateAndUnsetTokenField($request);

return $next($request, $response);
}

protected function _validateAndUnsetTokenField(ServerRequest &$request) {
if (in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH']) || $request->getData()) {
$this->_validateToken($request);
$body = $request->getParsedBody();
if (is_array($body)) {
unset($body[$this->_config['field']]);
$request = $request->withParsedBody($body);
}
}
}

/**
* Set the cookie in the response.
*
* Also sets the request->params['_csrfToken'] so the newly minted
* token is available in the request data.
*
* @param \Cake\Http\ServerRequest $request The request object.
* @param \Cake\Http\Response $response The response object.
* @return void
*/
protected function _setCookie(ServerRequest &$request, Response &$response)
{
$expiry = new Time($this->_config['expiry']);
$value = hash('sha512', Security::randomBytes(16), false);

$params = $request->getAttribute('params');
$params['_csrfToken'] = $value;
$request = $request->withAttribute('params', $params);

$response = $response->withCookie($this->_config['cookieName'], [
'value' => $value,
'expire' => $expiry->format('U'),
'path' => $request->getAttribute('webroot'),
'secure' => $this->_config['secure'],
'httpOnly' => $this->_config['httpOnly'],
]);
}

/**
* Validate the request data against the cookie token.
*
* @param \Cake\Http\ServerRequest $request The request to validate against.
* @throws \Cake\Network\Exception\InvalidCsrfTokenException when the CSRF token is invalid or missing.
* @return void
*/
protected function _validateToken(ServerRequest $request)
{
$cookies = $request->getCookieParams();
$cookie = isset($cookies[$this->_config['cookieName']]) ? $cookies[$this->_config['cookieName']] : null;
$post = $request->getData($this->_config['field']);
$header = $request->getHeaderLine('X-CSRF-Token');

if (!$cookie) {
throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie'));
}

if ($post !== $cookie && $header !== $cookie) {
throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.'));
}
}
}
74 changes: 74 additions & 0 deletions tests/TestCase/Http/Middleware/CsrfProtectionMiddlewareTest.php
@@ -0,0 +1,74 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Test\TestCase\Http\Middleware;

use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\ServerRequest;
use Cake\TestSuite\TestCase;
use Cake\Http\Response;

/**
* Test for CsrfProtection
*/
class CsrfProtectionMiddlewareTest extends TestCase
{

/**
* setup
*
* @return void
*/
public function setUp()
{
parent::setUp();
}

/**
* teardown
*
* @return void
*/
public function tearDown()
{
parent::tearDown();
}

/**
* Test setting the cookie value
*
* @return void
*/
public function testSettingCookie()
{
$request = new ServerRequest([
'environment' => ['REQUEST_METHOD' => 'GET'],
'webroot' => '/dir/',
]);
$response = new Response();

$callback = function($request, $response) {
return $response;
};
$middleware = new CsrfProtectionMiddleware();
$response = $middleware($request, $response, $callback);
$cookie = $response->cookie('csrfToken');

$this->assertNotEmpty($cookie, 'Should set a token.');
$this->assertRegExp('/^[a-f0-9]+$/', $cookie['value'], 'Should look like a hash.');
$this->assertEquals(0, $cookie['expire'], 'session duration.');
$this->assertEquals('/dir/', $cookie['path'], 'session path.');
$this->assertEquals($cookie['value'], $request->params['_csrfToken']);
}
}

0 comments on commit fe1c643

Please sign in to comment.