Skip to content

Commit

Permalink
Merge c14e49b into 101d4ca
Browse files Browse the repository at this point in the history
  • Loading branch information
markstory committed Jul 22, 2020
2 parents 101d4ca + c14e49b commit 9a884cf
Show file tree
Hide file tree
Showing 2 changed files with 574 additions and 0 deletions.
217 changes: 217 additions & 0 deletions src/Http/Middleware/SessionCsrfProtectionMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);

/**
* 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 4.2.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http\Middleware;

use ArrayAccess;
use Cake\Http\Exception\InvalidCsrfTokenException;
use Cake\Http\Session;
use Cake\Utility\Hash;
use Cake\Utility\Security;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;

/**
* Provides CSRF protection via session based tokens.
*
* This middleware adds a CSRF token to the session. Each request must
* contain a token in request data, or the X-CSRF-Token header on each PATCH, POST,
* PUT, or DELETE request. This follows a 'synchronizer token' pattern.
*
* If the request data is missing or does not match the session data,
* an InvalidCsrfTokenException will be raised.
*
* This middleware 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.
*
* If you use this middleware *do not* also use CsrfProtectionMiddleware.
*
* @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#sychronizer-token-pattern
*/
class SessionCsrfProtectionMiddleware implements MiddlewareInterface
{
/**
* Config for the CSRF handling.
*
* - `key` The session key to use. Defaults to `csrfToken`
* - `field` The form field to check. Changing this will also require configuring
* FormHelper.
*
* @var array
*/
protected $_config = [
'key' => 'csrfToken',
'field' => '_csrfToken',
];

/**
* Callback for deciding whether or not to skip the token check for particular request.
*
* CSRF protection token check will be skipped if the callback returns `true`.
*
* @var callable|null
*/
protected $skipCheckCallback;

/**
* @var int
*/
public const TOKEN_VALUE_LENGTH = 32;

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

/**
* Checks and sets the CSRF token depending on the HTTP verb.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
* @return \Psr\Http\Message\ResponseInterface A response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$method = $request->getMethod();
$hasData = in_array($method, ['PUT', 'POST', 'DELETE', 'PATCH'], true)
|| $request->getParsedBody();

if (
$hasData
&& $this->skipCheckCallback !== null
&& call_user_func($this->skipCheckCallback, $request) === true
) {
$request = $this->unsetTokenField($request);

return $handler->handle($request);
}

$session = $request->getAttribute('session');
if (!$session || !($session instanceof Session)) {
throw new RuntimeException('You must have a `session` attribute to use session based CSRF tokens');
}

$token = $session->read($this->_config['key']);
if ($token === null) {
$token = $this->createToken();
$session->write($this->_config['key'], $token);
}
$request = $request->withAttribute('csrfToken', $token);

if ($method === 'GET') {
return $handler->handle($request);
}

if ($hasData) {
$this->validateToken($request, $session);
$request = $this->unsetTokenField($request);
}

return $handler->handle($request);
}

/**
* Set callback for allowing to skip token check for particular request.
*
* The callback will receive request instance as argument and must return
* `true` if you want to skip token check for the current request.
*
* @param callable $callback A callable.
* @return $this
*/
public function skipCheckCallback(callable $callback)
{
$this->skipCheckCallback = $callback;

return $this;
}

/**
* Remove CSRF protection token from request data.
*
* This ensures that the token does not cause failures during
* form tampering protection.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request object.
* @return \Psr\Http\Message\ServerRequestInterface
*/
protected function unsetTokenField(ServerRequestInterface $request): ServerRequestInterface
{
$body = $request->getParsedBody();
if (is_array($body)) {
unset($body[$this->_config['field']]);
$request = $request->withParsedBody($body);
}

return $request;
}

/**
* Create a new token to be used for CSRF protection
*
* This token is a simple unique random value as the compare
* value is stored in the session where it cannot be tampered with.
*
* @return string
*/
public function createToken(): string
{
return Security::randomString(static::TOKEN_VALUE_LENGTH);
}

/**
* Validate the request data against the cookie token.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request to validate against.
* @param \Cake\Http\Session $session The session instance.
* @return void
* @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing.
*/
protected function validateToken(ServerRequestInterface $request, Session $session): void
{
$token = $session->read($this->_config['key']);
if (!$token || !is_string($token)) {
throw new InvalidCsrfTokenException(__d('cake', 'Missing or incorrect CSRF session key'));
}

$body = $request->getParsedBody();
if (is_array($body) || $body instanceof ArrayAccess) {
$post = (string)Hash::get($body, $this->_config['field']);
if (hash_equals($post, $token)) {
return;
}
}

$header = $request->getHeaderLine('X-CSRF-Token');
if (hash_equals($header, $token)) {
return;
}

throw new InvalidCsrfTokenException(__d(
'cake',
'CSRF token from either the request body or request headers did not match or is missing.'
));
}
}

0 comments on commit 9a884cf

Please sign in to comment.