diff --git a/core/core.services.yml b/core/core.services.yml index cd3ac50..43eb5ec 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -329,6 +329,11 @@ services: tags: - { name: event_subscriber } arguments: ['@settings'] + csrf_subscriber: + class: Drupal\Core\EventSubscriber\CsrfSubscriber + tags: + - { name: event_subscriber } + arguments: ['@csrf_token'] route_enhancer.authentication: class: Drupal\Core\Routing\Enhancer\AuthenticationEnhancer tags: @@ -497,6 +502,11 @@ services: - { name: path_processor_inbound, priority: 100 } - { name: path_processor_outbound, priority: 300 } arguments: ['@path.alias_manager'] + path_processor_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/PathProcessorCsrf.php b/core/lib/Drupal/Core/Access/PathProcessorCsrf.php new file mode 100644 index 0000000..49638b1 --- /dev/null +++ b/core/lib/Drupal/Core/Access/PathProcessorCsrf.php @@ -0,0 +1,51 @@ +csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($path, &$options = array(), Request $request = NULL, Route $route = NULL) { + if (isset($route)) { + if ($route->hasRequirement('_csrf')) { + $options['query']['csrf'] = $this->csrfToken->get($route->getRequirement('_csrf')); + } + } + + return $path; + } + +} + diff --git a/core/lib/Drupal/Core/EventSubscriber/CsrfSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/CsrfSubscriber.php new file mode 100644 index 0000000..3a95d5a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/CsrfSubscriber.php @@ -0,0 +1,69 @@ +csrfToken = $csrf_token; + } + + /** + * @todo + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestCsrfCheck(GetResponseEvent $event) { + $request = $event->getRequest(); + + if (!$request->attributes->has(RouteObjectInterface::ROUTE_OBJECT)) { + // If no Route is available it is likely a static resource and access is + // handled elsewhere. + return; + } + + $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); + + if ($route->hasRequirement('_csrf') && !$this->csrfToken->validate($request->query->get('csrf'), $route->getRequirement('_csrf'))) { + throw new AccessDeniedHttpException(); + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestCsrfCheck', 50); + return $events; + } + +} 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..50359a2 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -172,13 +172,18 @@ 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 (isset($options['query'])) { + $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . Url::buildQuery((array) $options['query']); + } $base_url = $this->context->getBaseUrl(); if (!$absolute || !$host = $this->context->getHost()) { return $base_url . $path . $fragment; @@ -261,7 +266,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 +323,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 +335,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/CsrfSubscriberTest.php b/core/tests/Drupal/Tests/Core/Access/CsrfSubscriberTest.php new file mode 100644 index 0000000..7a74182 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/CsrfSubscriberTest.php @@ -0,0 +1,122 @@ + 'CSRF access checker.', + 'description' => 'Tests CSRF access control for routes.', + 'group' => 'Routing', + ); + } + + public function setUp() { + $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator') + ->disableOriginalConstructor() + ->getMock(); + + $this->csrfSubscriber = new CsrfSubscriber($this->csrfToken); + $this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + } + + /** + * Tests the onKernelRequestCsrfCheck() with no route. + */ + public function testOnKernelRequestCsrfCheckNoRoute() { + $request = new Request(); + $event = new GetResponseEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $this->assertNull($this->csrfSubscriber->onKernelRequestCsrfCheck($event)); + } + + /** + * Tests the onKernelRequestCsrfCheck() with not route. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testOnKernelRequestCsrfCheckNoCsrfRequirement() { + $this->csrfToken->expects($this->once()) + ->method('validate') + ->will($this->returnValue(FALSE)); + + $route = new Route('', array(), array('_csrf' => 'test')); + $request = new Request(array(), array(), array(RouteObjectInterface::ROUTE_OBJECT => $route)); + $event = new GetResponseEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $this->csrfSubscriber->onKernelRequestCsrfCheck($event); + } + + /** + * Tests the onKernelRequestCsrfCheck() with a failing csrf check. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testOnKernelRequestCsrfCheckCsrfFail() { + $this->csrfToken->expects($this->once()) + ->method('validate') + ->will($this->returnValue(FALSE)); + + $route = new Route('', array(), array('_csrf' => 'test')); + $request = new Request(array(), array(), array(RouteObjectInterface::ROUTE_OBJECT => $route)); + $event = new GetResponseEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $this->csrfSubscriber->onKernelRequestCsrfCheck($event); + } + + /** + * Tests the onKernelRequestCsrfCheck() with a passing csrf check. + */ + public function testOnKernelRequestCsrfCheckCsrfPass() { + $this->csrfToken->expects($this->once()) + ->method('validate') + ->will($this->returnValue(TRUE)); + + $route = new Route('', array(), array('_csrf' => 'test')); + $request = new Request(array(), array(), array(RouteObjectInterface::ROUTE_OBJECT => $route)); + $event = new GetResponseEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $this->assertNull($this->csrfSubscriber->onKernelRequestCsrfCheck($event)); + } + +}