Skip to content
Snippets Groups Projects
Commit 62b7392d authored by catch's avatar catch
Browse files

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
Branches
Tags
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
Showing
with 302 additions and 24 deletions
...@@ -1100,6 +1100,11 @@ services: ...@@ -1100,6 +1100,11 @@ services:
tags: tags:
- { name: access_check, applies_to: _csrf_token, needs_incoming_request: TRUE } - { name: access_check, applies_to: _csrf_token, needs_incoming_request: TRUE }
arguments: ['@csrf_token'] arguments: ['@csrf_token']
access_check.header.csrf:
class: Drupal\Core\Access\CsrfRequestHeaderAccessCheck
arguments: ['@session_configuration', '@csrf_token']
tags:
- { name: access_check }
maintenance_mode: maintenance_mode:
class: Drupal\Core\Site\MaintenanceMode class: Drupal\Core\Site\MaintenanceMode
arguments: ['@state', '@current_user'] arguments: ['@state', '@current_user']
......
<?php <?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\AccountInterface;
use Drupal\Core\Session\SessionConfigurationInterface; use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
...@@ -12,7 +10,12 @@ ...@@ -12,7 +10,12 @@
/** /**
* Access protection against CSRF attacks. * 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. * The session configuration.
...@@ -21,14 +24,24 @@ class CSRFAccessCheck implements AccessCheckInterface { ...@@ -21,14 +24,24 @@ class CSRFAccessCheck implements AccessCheckInterface {
*/ */
protected $sessionConfiguration; protected $sessionConfiguration;
/**
* The token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/** /**
* Constructs a new rest CSRF access check. * Constructs a new rest CSRF access check.
* *
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The 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->sessionConfiguration = $session_configuration;
$this->csrfToken = $csrf_token;
} }
/** /**
...@@ -36,8 +49,16 @@ public function __construct(SessionConfigurationInterface $session_configuration ...@@ -36,8 +49,16 @@ public function __construct(SessionConfigurationInterface $session_configuration
*/ */
public function applies(Route $route) { public function applies(Route $route) {
$requirements = $route->getRequirements(); $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'])) { if (isset($requirements['_method'])) {
// There could be more than one method requirement separated with '|'. // There could be more than one method requirement separated with '|'.
$methods = explode('|', $requirements['_method']); $methods = explode('|', $requirements['_method']);
...@@ -77,7 +98,10 @@ public function access(Request $request, AccountInterface $account) { ...@@ -77,7 +98,10 @@ public function access(Request $request, AccountInterface $account) {
&& $this->sessionConfiguration->hasSession($request) && $this->sessionConfiguration->hasSession($request)
) { ) {
$csrf_token = $request->headers->get('X-CSRF-Token'); $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); 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: rest.csrftoken:
path: '/rest/session/token' path: '/rest/session/token'
defaults: defaults:
_controller: '\Drupal\rest\RequestHandler::csrfToken' _controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements: requirements:
_access: 'TRUE' _access: 'TRUE'
...@@ -8,11 +8,9 @@ services: ...@@ -8,11 +8,9 @@ services:
- { name: cache.bin } - { name: cache.bin }
factory: cache_factory:get factory: cache_factory:get
arguments: [rest] arguments: [rest]
# @todo Remove this service in Drupal 9.0.0.
access_check.rest.csrf: access_check.rest.csrf:
class: Drupal\rest\Access\CSRFAccessCheck alias: access_check.header.csrf
arguments: ['@session_configuration']
tags:
- { name: access_check }
rest.link_manager: rest.link_manager:
class: Drupal\rest\LinkManager\LinkManager class: Drupal\rest\LinkManager\LinkManager
arguments: ['@rest.link_manager.type', '@rest.link_manager.relation'] arguments: ['@rest.link_manager.type', '@rest.link_manager.relation']
......
...@@ -181,16 +181,6 @@ protected function getResponseFormat(RouteMatchInterface $route_match, Request $ ...@@ -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. * Renders a resource response.
* *
......
...@@ -91,7 +91,7 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_ ...@@ -91,7 +91,7 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
$methods = $route->getMethods(); $methods = $route->getMethods();
// Only expose routes where the method is enabled in the configuration. // Only expose routes where the method is enabled in the configuration.
if ($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) { 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. // Check that authentication providers are defined.
if (empty($rest_resource_config->getAuthenticationProviders($method))) { if (empty($rest_resource_config->getAuthenticationProviders($method))) {
......
...@@ -72,6 +72,10 @@ public function testBasicAuth() { ...@@ -72,6 +72,10 @@ public function testBasicAuth() {
/** /**
* Tests that CSRF check is triggered for Cookie Auth requests. * 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() { public function testCookieAuth() {
$this->drupalLogin($this->account); $this->drupalLogin($this->account);
......
...@@ -95,7 +95,7 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) { ...@@ -95,7 +95,7 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
} }
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) { if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
// GET the CSRF token first for writing requests. // GET the CSRF token first for writing requests.
$token = $this->drupalGet('rest/session/token'); $token = $this->drupalGet('session/token');
} }
$url = $this->buildUrl($url); $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: ...@@ -490,3 +490,10 @@ system.entity_autocomplete:
_controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete' _controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete'
requirements: requirements:
_access: 'TRUE' _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());
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment