diff --git a/core/core.services.yml b/core/core.services.yml index 6a0669c..23266fe 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -241,7 +241,7 @@ services: - [setFinalMatcher, ['@router.matcher.final_matcher']] url_generator: class: Drupal\Core\Routing\UrlGenerator - arguments: ['@router.route_provider', '@path_processor_manager', '@config.factory', '@settings'] + arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@settings'] calls: - [setRequest, ['@?request']] - [setContext, ['@?router.request_context']] @@ -418,6 +418,11 @@ services: class: Drupal\Core\Theme\ThemeAccessCheck 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: @@ -480,6 +485,8 @@ services: tags: - { name: event_subscriber } arguments: [['@exception_controller', execute]] + route_processor_manager: + class: Drupal\Core\RouteProcessor\RouteProcessorManager path_processor_manager: class: Drupal\Core\PathProcessor\PathProcessorManager path_processor_decode: @@ -498,6 +505,11 @@ services: - { name: path_processor_inbound, priority: 100 } - { name: path_processor_outbound, priority: 300 } arguments: ['@path.alias_manager'] + route_processer_csrf: + class: Drupal\Core\Access\RouteProcessorCsrf + tags: + - { name: route_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..09364c2 --- /dev/null +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php @@ -0,0 +1,68 @@ +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) { + // If this is not the controller request ALLOW now. + if ($request->attributes->get('_controller_request')) { + $conjunction = $route->getOption('_access_mode') ?: 'ANY'; + // Return ALLOW if all access checks are needed. + if ($conjunction == 'ALL') { + return static::ALLOW; + } + // Return DENY otherwise, as another access checker should grant access + // for the route. + else { + return static::DENY; + } + } + + return $this->csrfToken->validate($request->query->get('csrf'), $route->getRequirement('_csrf')) ? static::ALLOW : static::KILL; + } + +} diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php new file mode 100644 index 0000000..49cef65 --- /dev/null +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -0,0 +1,49 @@ +csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function processOutbound(Route $route, array &$parameters) { + if ($route->hasRequirement('_csrf')) { + // Adding this to the parameters means it will get merged into the query + // string when the route is compiled. + $parameters['csrf'] = $this->csrfToken->get($route->getRequirement('_csrf')); + } + } + +} + diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 9cd8f5a..8a525c9 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -14,6 +14,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterRouteProcessorsPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; @@ -63,6 +64,7 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new RegisterServicesForDestructionPass()); // Add the compiler pass that will process the tagged services. $container->addCompilerPass(new RegisterPathProcessorsPass()); + $container->addCompilerPass(new RegisterRouteProcessorsPass()); $container->addCompilerPass(new ListCacheBinsPass()); // Add the compiler pass for appending string translators. $container->addCompilerPass(new RegisterStringTranslatorsPass()); diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php new file mode 100644 index 0000000..647a725 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php @@ -0,0 +1,36 @@ +hasDefinition('route_processor_manager')) { + return; + } + $manager = $container->getDefinition('route_processor_manager'); + // Add outbound route processors. + foreach ($container->findTaggedServiceIds('route_processor_outbound') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $manager->addMethodCall('addOutbound', array(new Reference($id), $priority)); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php index 6f18839..e6f6eb8 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -9,6 +9,7 @@ use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -40,6 +41,11 @@ public function __construct(AccessManager $access_manager) { */ public function onKernelRequestAccessCheck(GetResponseEvent $event) { $request = $event->getRequest(); + + // The controller is being handled by the HTTP kernel, so add an attribute + // to tell us this is the controller request. + $request->attributes->set('_controller_request', TRUE); + if (!$request->attributes->has(RouteObjectInterface::ROUTE_OBJECT)) { // If no Route is available it is likely a static resource and access is // handled elsewhere. @@ -50,6 +56,8 @@ public function onKernelRequestAccessCheck(GetResponseEvent $event) { if (!$access) { throw new AccessDeniedHttpException(); } + + $request->attributes->remove('_controller_request'); } /** diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php index 347a877..9e69001 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. diff --git a/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php new file mode 100644 index 0000000..8f421b3 --- /dev/null +++ b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php @@ -0,0 +1,32 @@ +outboundProcessors[$priority][] = $processor; + $this->sortedOutbound = array(); + } + + /** + * {@inheritdoc} + */ + public function processOutbound(Route $route, array &$parameters) { + $processors = $this->getOutbound(); + foreach ($processors as $processor) { + $processor->processOutbound($route, $parameters); + } + } + + /** + * Returns the sorted array of outbound processors. + * + * @return array + * An array of processor objects. + */ + protected function getOutbound() { + if (empty($this->sortedOutbound)) { + $this->sortedOutbound = $this->sortProcessors(); + } + + return $this->sortedOutbound; + } + + /** + * Sorts the processors according to priority. + */ + protected function sortProcessors() { + $sorted = array(); + krsort($this->outboundProcessors); + + foreach ($this->outboundProcessors as $processors) { + $sorted = array_merge($sorted, $processors); + } + return $sorted; + } + +} 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 540a580..95c776c 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -19,6 +19,7 @@ use Drupal\Component\Utility\Url; use Drupal\Core\Config\ConfigFactory; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; +use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; /** * Generates URLs from route names and parameters. @@ -40,6 +41,13 @@ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterfa protected $pathProcessor; /** + * The route processor. + * + * @var \Drupal\Tests\Core\RouteProcessor\OutboundRouteProcessorInterface + */ + protected $routeProcessor; + + /** * The base path to use for urls. * * @var string @@ -77,10 +85,11 @@ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterfa * @param \Symfony\Component\HttpKernel\Log\LoggerInterface $logger * An optional logger for recording errors. */ - public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, ConfigFactory $config, Settings $settings, LoggerInterface $logger = NULL) { + public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, OutboundRouteProcessorInterface $route_processor, ConfigFactory $config, Settings $settings, LoggerInterface $logger = NULL) { parent::__construct($provider, $logger); $this->pathProcessor = $path_processor; + $this->routeProcessor = $route_processor; $this->mixedModeSessions = $settings->get('mixed_mode_sessions', FALSE); $allowed_protocols = $config->get('system.filter')->get('protocols') ?: array('http', 'https'); Url::setAllowedProtocols($allowed_protocols); @@ -167,10 +176,13 @@ public function generate($name, $parameters = array(), $absolute = FALSE) { public function generateFromRoute($name, $parameters = array(), $options = array()) { $absolute = !empty($options['absolute']); $route = $this->getRoute($name); + $this->processRoute($route, $parameters); + // Symfony adds any parameters that are not path slugs as query strings. if (isset($options['query']) && is_array($options['query'])) { $parameters = (array) $parameters + $options['query']; } + $path = $this->getInternalPathFromRoute($route, $parameters); $path = $this->processPath($path, $options); $fragment = ''; @@ -179,6 +191,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array $fragment = '#' . $fragment; } } + $base_url = $this->context->getBaseUrl(); if (!$absolute || !$host = $this->context->getHost()) { return $base_url . $path . $fragment; @@ -336,6 +349,19 @@ protected function processPath($path, &$options = array()) { } /** + * Passes the route to the processor manager for altering before complation. + * + * @param \Symfony\Component\Routing\Route $route + * The route object to process. + * + * @param array $parameters + * An array of parameters to be passed to the route compiler. + */ + protected function processRoute(SymfonyRoute $route, array &$parameters) { + $this->routeProcessor->processOutbound($route, $parameters); + } + + /** * Returns whether or not the url generator has been initialized. * * @return bool 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..764797c --- /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); + } + +}