diff --git a/core/core.services.yml b/core/core.services.yml index f941e33..8fdeb12 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -409,6 +409,11 @@ services: arguments: ['@plugin.manager.entity'] tags: - { name: access_check } + access_check.csrf: + class: Drupal\Core\Access\CsrfAccessCheck + tags: + - { name: access_check } + arguments: ['@csrf_token'] maintenance_mode_subscriber: class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber tags: @@ -489,6 +494,11 @@ services: - { name: path_processor_inbound, priority: 100 } - { name: path_processor_outbound, priority: 300 } arguments: ['@path.alias_manager'] + path_processer_csrf: + class: Drupal\Core\Access\PathProcessorCsrf + tags: + - { name: path_processor_outbound, priority: 400 } + arguments: ['@csrf_token'] transliteration: class: Drupal\Core\Transliteration\PHPTransliteration flood: diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php new file mode 100644 index 0000000..3958eda --- /dev/null +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php @@ -0,0 +1,54 @@ +get() using the same value as the + * "_csrf" parameter in the route. + */ +class CsrfAccessCheck implements StaticAccessCheckInterface { + + /** + * The CSRF token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator + */ + protected $csrfToken; + + /** + * Constructs a CsrfAccessCheck object. + * + * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token + * The CSRF token generator. + */ + function __construct(CsrfTokenGenerator $csrf_token) { + $this->csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function appliesTo() { + return array('_csrf'); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request) { + return $this->csrfToken->validate($request->query->get('csrf'), $route->getRequirement('_csrf')) ? static::ALLOW : static::KILL; + } + +} diff --git a/core/lib/Drupal/Core/Access/PathProcessorCsrf.php b/core/lib/Drupal/Core/Access/PathProcessorCsrf.php new file mode 100644 index 0000000..36fdb46 --- /dev/null +++ b/core/lib/Drupal/Core/Access/PathProcessorCsrf.php @@ -0,0 +1,52 @@ +csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { + if (isset($route)) { + $requirements = $route->getRequirements(); + if (array_key_exists('_csrf', $requirements)) { + $options['query']['csrf'] = $this->csrfToken->get($requirements['_csrf']); + } + } + + return $path; + } + +} + diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php index 347a877..1ca33af 100644 --- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php @@ -8,6 +8,7 @@ namespace Drupal\Core\PathProcessor; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Defines an interface for classes that process the outbound path. @@ -27,9 +28,12 @@ * @param \Symfony\Component\HttpFoundation\Request $request * The HttpRequest object representing the current request. * + * @param \Symfony\Component\Routing\Route $route + * The route object currently being processed. + * * @return * The processed path. */ - public function processOutbound($path, &$options = array(), Request $request = NULL); + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL); } diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php index cfcf32e..ba7e7f7 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php @@ -9,6 +9,7 @@ use Drupal\Core\Path\AliasManagerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Processes the inbound path using path alias lookups. @@ -43,7 +44,7 @@ public function processInbound($path, Request $request) { /** * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). */ - public function processOutbound($path, &$options = array(), Request $request = NULL) { + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { $langcode = isset($options['language']) ? $options['language']->id : NULL; $path = $this->aliasManager->getPathAlias($path, $langcode); return $path; diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php index d03d19b..5705934 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php @@ -9,6 +9,7 @@ use Drupal\Core\Config\ConfigFactory; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Processes the inbound path by resolving it to the front page if empty. @@ -48,7 +49,7 @@ public function processInbound($path, Request $request) { /** * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). */ - public function processOutbound($path, &$options = array(), Request $request = NULL) { + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { // The special path '' links to the default front page. if ($path == '') { $path = ''; diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php index db8b799..f0c9737 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php @@ -8,6 +8,7 @@ namespace Drupal\Core\PathProcessor; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Path processor manager. @@ -109,10 +110,10 @@ public function addOutbound(OutboundPathProcessorInterface $processor, $priority /** * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). */ - public function processOutbound($path, &$options = array(), Request $request = NULL) { + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { $processors = $this->getOutbound(); foreach ($processors as $processor) { - $path = $processor->processOutbound($path, $options, $request); + $path = $processor->processOutbound($path, $options, $request, $route); } return $path; } diff --git a/core/lib/Drupal/Core/Routing/NullGenerator.php b/core/lib/Drupal/Core/Routing/NullGenerator.php index b6e2609..61e30bc 100644 --- a/core/lib/Drupal/Core/Routing/NullGenerator.php +++ b/core/lib/Drupal/Core/Routing/NullGenerator.php @@ -9,6 +9,7 @@ use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Route; /** * No-op implementation of a Url Generator, needed for backward compatibility. @@ -46,7 +47,7 @@ public function getContext() { /** * Overrides Drupal\Core\Routing\UrlGenerator::processPath(). */ - protected function processPath($path, &$options = array()) { + protected function processPath($path, &$options = array(), Route $route = NULL) { return $path; } } diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 53ac95d..91c89c2 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -172,13 +172,17 @@ public function generateFromRoute($name, $parameters = array(), $options = array $parameters = (array) $parameters + $options['query']; } $path = $this->getInternalPathFromRoute($route, $parameters); - $path = $this->processPath($path, $options); + $path = $this->processPath($path, $options, $route); $fragment = ''; if (isset($options['fragment'])) { if (($fragment = trim($options['fragment'])) != '') { $fragment = '#' . $fragment; } } + // Append the query. + if ($options['query']) { + $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . Url::buildQuery($options['query']); + } $base_url = $this->context->getBaseUrl(); if (!$absolute || !$host = $this->context->getHost()) { return $base_url . $path . $fragment; @@ -261,7 +265,7 @@ public function generateFromPath($path = NULL, $options = array()) { return $path . $options['fragment']; } else { - $path = ltrim($this->processPath($path, $options), '/'); + $path = ltrim($this->processPath($path, $options, NULL), '/'); } if (!isset($options['script'])) { @@ -318,7 +322,7 @@ public function setScriptPath($path) { /** * Passes the path to a processor manager to allow alterations. */ - protected function processPath($path, &$options = array()) { + protected function processPath($path, &$options = array(), SymfonyRoute $route = NULL) { // Router-based paths may have a querystring on them. if ($query_pos = strpos($path, '?')) { // We don't need to do a strict check here because position 0 would mean we @@ -330,7 +334,7 @@ protected function processPath($path, &$options = array()) { $actual_path = $path; $query_string = ''; } - $path = '/' . $this->pathProcessor->processOutbound(trim($actual_path, '/'), $options, $this->request); + $path = '/' . $this->pathProcessor->processOutbound(trim($actual_path, '/'), $options, $this->request, $route); $path .= $query_string; return $path; } diff --git a/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php index aa3cc2b..b9aa26b 100644 --- a/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php +++ b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php @@ -15,6 +15,7 @@ use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Processes the inbound path using path alias lookups. @@ -93,7 +94,7 @@ public function processInbound($path, Request $request) { /** * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processOutbound(). */ - public function processOutbound($path, &$options = array(), Request $request = NULL) { + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { if (!$this->languageManager->isMultilingual()) { return $path; } diff --git a/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php b/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php index 644dd49..942a8d7 100644 --- a/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php +++ b/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php @@ -10,6 +10,7 @@ use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Path processor for url_alter_test. @@ -42,7 +43,7 @@ public function processInbound($path, Request $request) { /** * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). */ - public function processOutbound($path, &$options = array(), Request $request = NULL) { + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { // Rewrite user/uid to user/username. if (preg_match('!^user/([0-9]+)(/.*)?!', $path, $matches)) { if ($account = user_load($matches[1])) { diff --git a/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php new file mode 100644 index 0000000..d094aa6 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php @@ -0,0 +1,61 @@ + 'CSRF access checker.', + 'description' => 'Tests CSRF access control for routes.', + 'group' => 'Routing', + ); + } + + /** + * Tests CsrfAccessCheck::appliesTo(). + */ + public function testAppliesTo() { + $this->assertEquals($this->accessChecker->appliesTo(), array('_csrf'), 'Access checker returned the expected appliesTo() array.'); + } + + /** + * Tests CsrfAccessCheck::access(). + */ + public function testAccess() { + $token_value = 'b1rd'; + $route = new Route('/foo', array(), array('_csrf' => $token_value)); + $request = new Request(array( + 'csrf' => drupal_get_token($token_value), + )); + $access_check = new CsrfAccessCheck(); + $access = $access_check->access($route, $request); + $this->assertEquals(AccessCheckInterface::ALLOW, $access); + + // Run the same request with an invalid token. + $route = new Route('/foo', array(), array('_csrf: ' . $token_value)); + $request = new Request(array( + 'csrf' => $token_value, + )); + $access_check = new CsrfAccessCheck(); + $access = $access_check->access($route, $request); + $this->assertEquals(AccessCheckInterface::KILL, $access); + } + +}