Commit 62b7392d authored by catch's avatar catch

Issue #2753681 by tedbow, Wim Leers, dawehner, neclimdul, catch: Move CSRF...

Issue #2753681 by tedbow, Wim Leers, dawehner, neclimdul, catch: Move CSRF header token out of REST module so that user module can use it, as well as any contrib module
parent 3f56402e
......@@ -1100,6 +1100,11 @@ services:
tags:
- { name: access_check, applies_to: _csrf_token, needs_incoming_request: TRUE }
arguments: ['@csrf_token']
access_check.header.csrf:
class: Drupal\Core\Access\CsrfRequestHeaderAccessCheck
arguments: ['@session_configuration', '@csrf_token']
tags:
- { name: access_check }
maintenance_mode:
class: Drupal\Core\Site\MaintenanceMode
arguments: ['@state', '@current_user']
......
<?php
namespace Drupal\rest\Access;
namespace Drupal\Core\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\Routing\Route;
......@@ -12,7 +10,12 @@
/**
* Access protection against CSRF attacks.
*/
class CSRFAccessCheck implements AccessCheckInterface {
class CsrfRequestHeaderAccessCheck implements AccessCheckInterface {
/**
* A string key that will used to designate the token used by this class.
*/
const TOKEN_KEY = 'X-CSRF-Token request header';
/**
* The session configuration.
......@@ -21,14 +24,24 @@ class CSRFAccessCheck implements AccessCheckInterface {
*/
protected $sessionConfiguration;
/**
* The token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a new rest CSRF access check.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The token generator.
*/
public function __construct(SessionConfigurationInterface $session_configuration) {
public function __construct(SessionConfigurationInterface $session_configuration, CsrfTokenGenerator $csrf_token) {
$this->sessionConfiguration = $session_configuration;
$this->csrfToken = $csrf_token;
}
/**
......@@ -36,8 +49,16 @@ public function __construct(SessionConfigurationInterface $session_configuration
*/
public function applies(Route $route) {
$requirements = $route->getRequirements();
// Check for current requirement _csrf_request_header_token and deprecated
// REST requirement.
$applicable_requirements = [
'_csrf_request_header_token',
// @todo Remove _access_rest_csrf in Drupal 9.0.0.
'_access_rest_csrf',
];
$requirement_keys = array_keys($requirements);
if (array_key_exists('_access_rest_csrf', $requirements)) {
if (array_intersect($applicable_requirements, $requirement_keys)) {
if (isset($requirements['_method'])) {
// There could be more than one method requirement separated with '|'.
$methods = explode('|', $requirements['_method']);
......@@ -77,7 +98,10 @@ public function access(Request $request, AccountInterface $account) {
&& $this->sessionConfiguration->hasSession($request)
) {
$csrf_token = $request->headers->get('X-CSRF-Token');
if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) {
// @todo Remove validate call using 'rest' in 8.3.
// Kept here for sessions active during update.
if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
&& !$this->csrfToken->validate($csrf_token, 'rest')) {
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
......
# @deprecated This route is deprecated, use the system.csrftoken route from the
# system module instead.
# @todo Remove this route in Drupal 9.0.0.
rest.csrftoken:
path: '/rest/session/token'
defaults:
_controller: '\Drupal\rest\RequestHandler::csrfToken'
_controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'
......@@ -8,11 +8,9 @@ services:
- { name: cache.bin }
factory: cache_factory:get
arguments: [rest]
# @todo Remove this service in Drupal 9.0.0.
access_check.rest.csrf:
class: Drupal\rest\Access\CSRFAccessCheck
arguments: ['@session_configuration']
tags:
- { name: access_check }
alias: access_check.header.csrf
rest.link_manager:
class: Drupal\rest\LinkManager\LinkManager
arguments: ['@rest.link_manager.type', '@rest.link_manager.relation']
......
......@@ -181,16 +181,6 @@ protected function getResponseFormat(RouteMatchInterface $route_match, Request $
}
}
/**
* Generates a CSRF protecting session token.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function csrfToken() {
return new Response(\Drupal::csrfToken()->get('rest'), 200, array('Content-Type' => 'text/plain'));
}
/**
* Renders a resource response.
*
......
......@@ -91,7 +91,7 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
$methods = $route->getMethods();
// Only expose routes where the method is enabled in the configuration.
if ($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) {
$route->setRequirement('_access_rest_csrf', 'TRUE');
$route->setRequirement('_csrf_request_header_token', 'TRUE');
// Check that authentication providers are defined.
if (empty($rest_resource_config->getAuthenticationProviders($method))) {
......
......@@ -72,6 +72,10 @@ public function testBasicAuth() {
/**
* Tests that CSRF check is triggered for Cookie Auth requests.
*
* @deprecated as of Drupal 8.2.x, will be removed before Drupal 9.0.0. Use
* \Drupal\Tests\system\Functional\CsrfRequestHeaderTest::testRouteAccess
* instead.
*/
public function testCookieAuth() {
$this->drupalLogin($this->account);
......
......@@ -95,7 +95,7 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
}
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
// GET the CSRF token first for writing requests.
$token = $this->drupalGet('rest/session/token');
$token = $this->drupalGet('session/token');
}
$url = $this->buildUrl($url);
......
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Access\CsrfRequestHeaderAccessCheck;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Returns responses for CSRF token routes.
*/
class CsrfTokenController implements ContainerInjectionInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $tokenGenerator;
/**
* Constructs a new CsrfTokenController object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
* The CSRF token generator.
*/
public function __construct(CsrfTokenGenerator $token_generator) {
$this->tokenGenerator = $token_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('csrf_token')
);
}
/**
* Returns a CSRF protecting session token.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function csrfToken() {
return new Response($this->tokenGenerator->get(CsrfRequestHeaderAccessCheck::TOKEN_KEY), 200, ['Content-Type' => 'text/plain']);
}
}
......@@ -490,3 +490,10 @@ system.entity_autocomplete:
_controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete'
requirements:
_access: 'TRUE'
system.csrftoken:
path: '/session/token'
defaults:
_controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'
name: CSRF test
type: module
description: 'Support testing protecting routes with CSRF token.'
package: Testing
version: VERSION
core: 8.x
# Tests CSRF request header token protection.
csrf_test.protected:
path: csrf/protected
defaults:
_controller: '\Drupal\csrf_test\Controller\TestController::testMethod'
requirements:
_csrf_request_header_token: 'TRUE'
_method: 'POST'
# Tests deprecated _access_rest_csrf protection.
# This originally was in the REST module but now is supported in core/lib.
# @see https://www.drupal.org/node/2753681
# @todo Remove this test route in Drupal 9.0.0.
csrf_test.deprecated.protected:
path: csrf/deprecated/protected
defaults:
_controller: '\Drupal\csrf_test\Controller\TestController::testMethod'
requirements:
_access_rest_csrf: 'TRUE'
_method: 'POST'
# @todo This route can be removed in 8.3.
# @see \Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
csrf_test.deprecated.csrftoken:
path: '/deprecated/session/token'
defaults:
_controller: '\Drupal\csrf_test\Controller\DeprecatedCsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'
<?php
namespace Drupal\csrf_test\Controller;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Returns responses for Deprecated CSRF token routes.
*
* This controller tests using the deprecated CSRF token key 'rest'.
*
* @todo This class can be removed in 8.3.
*
* @see \Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
*/
class DeprecatedCsrfTokenController implements ContainerInjectionInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $tokenGenerator;
/**
* Constructs a new CsrfTokenController object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
* The CSRF token generator.
*/
public function __construct(CsrfTokenGenerator $token_generator) {
$this->tokenGenerator = $token_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('csrf_token')
);
}
/**
* Returns a CSRF using the deprecated 'rest' value protecting session token.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function csrfToken() {
return new Response($this->tokenGenerator->get('rest'), 200, ['Content-Type' => 'text/plain']);
}
}
<?php
namespace Drupal\csrf_test\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* Just a test controller for test routes.
*/
class TestController {
/**
* Just a test method for the test routes.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function testMethod() {
return new Response('Sometimes it is hard to think of test content!');
}
}
<?php
namespace Drupal\Tests\system\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use GuzzleHttp\Cookie\CookieJar;
/**
* Tests protecting routes by requiring CSRF token in the request header.
*
* @group system
*/
class CsrfRequestHeaderTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['system', 'csrf_test'];
/**
* Tests access to routes protected by CSRF request header requirements.
*
* This checks one route that uses _csrf_request_header_token and one that
* uses the deprecated _access_rest_csrf.
*/
public function testRouteAccess() {
$client = \Drupal::httpClient();
$csrf_token_paths = ['deprecated/session/token', 'session/token'];
// Test using the both the current path and a test path that returns
// a token using the deprecated 'rest' value.
// Checking /deprecated/session/token can be removed in 8.3.
// @see \Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
foreach ($csrf_token_paths as $csrf_token_path) {
// Check both test routes.
$route_names = ['csrf_test.protected', 'csrf_test.deprecated.protected'];
foreach ($route_names as $route_name) {
$user = $this->drupalCreateUser();
$this->drupalLogin($user);
$csrf_token = $this->drupalGet($csrf_token_path);
$url = Url::fromRoute($route_name)
->setAbsolute(TRUE)
->toString();
$domain = parse_url($url, PHP_URL_HOST);
$session_id = $this->getSession()->getCookie($this->getSessionName());
/** @var \GuzzleHttp\Cookie\CookieJar $cookies */
$cookies = CookieJar::fromArray([$this->getSessionName() => $session_id], $domain);
$post_options = [
'headers' => ['Accept' => 'text/plain'],
'http_errors' => FALSE,
];
// Test that access is allowed for anonymous user with no token in header.
$result = $client->post($url, $post_options);
$this->assertEquals(200, $result->getStatusCode());
// Add cookies to POST options so that all other requests are for the
// authenticated user.
$post_options['cookies'] = $cookies;
// Test that access is denied with no token in header.
$result = $client->post($url, $post_options);
$this->assertEquals(403, $result->getStatusCode());
// Test that access is allowed with correct token in header.
$post_options['headers']['X-CSRF-Token'] = $csrf_token;
$result = $client->post($url, $post_options);
$this->assertEquals(200, $result->getStatusCode());
// Test that access is denied with incorrect token in header.
$post_options['headers']['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for';
$result = $client->post($url, $post_options);
$this->assertEquals(403, $result->getStatusCode());
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment