diff --git a/core/core.services.yml b/core/core.services.yml
index 37a1c482d334a6a2817406576e7058031c9e263b..8b4eed020ab46897e694c926e23bb3b59050b90f 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -264,7 +264,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']]
@@ -449,6 +449,11 @@ services:
     arguments: ['@controller_resolver']
     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:
@@ -507,6 +512,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:
@@ -525,6 +532,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 }
+    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 0000000000000000000000000000000000000000..d6ba3a846e22cf62492241f3c5348dbdc8b6f4ce
--- /dev/null
+++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Access\CsrfAccessCheck.
+ */
+
+namespace Drupal\Core\Access;
+
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Allows access to routes to be controlled by a '_csrf_token' parameter.
+ *
+ * To use this check, add a "token" GET parameter to URLs of which the value is
+ * a token generated by \Drupal::csrfToken()->get() using the same value as the
+ * "_csrf_token" 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_token');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    // If this is the controller request, check CSRF access as normal.
+    if ($request->attributes->get('_controller_request')) {
+      return $this->csrfToken->validate($request->query->get('token'), $route->getRequirement('_csrf_token')) ? static::ALLOW : static::KILL;
+    }
+
+    // Otherwise, this could be another requested access check that we don't
+    // want to check CSRF tokens on.
+    $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;
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
new file mode 100644
index 0000000000000000000000000000000000000000..0fb075cedfd9cc0dc5c30fde2149576178fbd934
--- /dev/null
+++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Access\RouteProcessorCsrf.
+ */
+
+namespace Drupal\Core\Access;
+
+use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Processes the inbound path by resolving it to the front page if empty.
+ */
+class RouteProcessorCsrf implements OutboundRouteProcessorInterface {
+
+  /**
+   * The CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfToken;
+
+  /**
+   * Constructs a RouteProcessorCsrf object.
+   *
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
+   *   The CSRF token generator.
+   */
+  function __construct(CsrfTokenGenerator $csrf_token) {
+    $this->csrfToken = $csrf_token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processOutbound(Route $route, array &$parameters) {
+    if ($route->hasRequirement('_csrf_token')) {
+      // Adding this to the parameters means it will get merged into the query
+      // string when the route is compiled.
+      $parameters['token'] = $this->csrfToken->get($route->getRequirement('_csrf_token'));
+    }
+  }
+
+}
+
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 9cd8f5a87bbd62d790e5a096952b72d26edc6800..8a525c9abdbcd96eaa0d80cfe5642ba41408399c 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 0000000000000000000000000000000000000000..dd958695b1932c9d302a681566ddb64bb1763385
--- /dev/null
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\DependencyInjection\Compiler\RegisterRouteProcessorsPass.
+ */
+
+namespace Drupal\Core\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+/**
+ * Adds services to the route_processor_manager service.
+ */
+class RegisterRouteProcessorsPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(ContainerBuilder $container) {
+    if (!$container->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 09261a6c509887cc9a22e9ef9bda188e004335ab..0c319994395bd6adca01c2f7463ea155102a868a 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Session\AccountInterface;
 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;
@@ -59,13 +60,29 @@ public function __construct(AccessManager $access_manager, AccountInterface $cur
    */
   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.
       return;
     }
 
-    $access = $this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT), $request, $this->currentUser);
+    // Wrap this in a try/catch to ensure the '_controller_request' attribute
+    // can always be removed.
+    try {
+      $access = $this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT), $request, $this->currentUser);
+    }
+    catch (\Exception $e) {
+      $request->attributes->remove('_controller_request');
+      throw $e;
+    }
+
+    $request->attributes->remove('_controller_request');
+
     if (!$access) {
       throw new AccessDeniedHttpException();
     }
diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
index 347a8777574bcd5e7be655aef30afd1dad5c0b1f..9e6900181498bb2a4b4bffb47920c1eb25f4a6ad 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 0000000000000000000000000000000000000000..c9bda247fe8dec9788f97afd57b721a726c5806f
--- /dev/null
+++ b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface.
+ */
+
+namespace Drupal\Core\RouteProcessor;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Defines an interface for classes that process the outbound route.
+ */
+interface OutboundRouteProcessorInterface {
+
+  /**
+   * Processes the outbound route.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The outbound route to process.
+   *
+   * @param array $parameters
+   *   An array of parameters to be passed to the route compiler. Passed by
+   *   reference.
+   *
+   * @return
+   *   The processed path.
+   */
+  public function processOutbound(Route $route, array &$parameters);
+
+}
diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..43071be3d739df0c506182e63ce44a3221861d49
--- /dev/null
+++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\RouteProcessor\RouteProcessorManager.
+ */
+
+namespace Drupal\Core\RouteProcessor;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Route processor manager.
+ *
+ * Holds an array of route processor objects and uses them to sequentially
+ * process an outbound route, in order of processor priority.
+ */
+class RouteProcessorManager implements OutboundRouteProcessorInterface {
+
+  /**
+   * Holds the array of outbound processors to cycle through.
+   *
+   * @var array
+   *   An array whose keys are priorities and whose values are arrays of path
+   *   processor objects.
+   */
+  protected $outboundProcessors = array();
+
+  /**
+   * Holds the array of outbound processors, sorted by priority.
+   *
+   * @var array
+   *   An array of path processor objects.
+   */
+  protected $sortedOutbound = array();
+
+  /**
+   * Adds an outbound processor object to the $outboundProcessors property.
+   *
+   * @param \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface $processor
+   *   The processor object to add.
+   *
+   * @param int $priority
+   *   The priority of the processor being added.
+   */
+  public function addOutbound(OutboundRouteProcessorInterface $processor, $priority = 0) {
+    $this->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 b6e26097fde15a11c7ab8e8c7d5648540effef02..1430f1f744dce5e1622c2738f8151ed0973e36c2 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.
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index bf6956fd0877b23583a00d9d4a0a8aef883a8f08..1bb4b53b50353253688af34f860a06c9eed76034 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.
@@ -39,6 +40,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.
    *
@@ -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;
@@ -335,6 +348,19 @@ protected function processPath($path, &$options = array()) {
     return $path;
   }
 
+  /**
+   * 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.
    *
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php
index 7123ae57e20b2eb85066ffce940e71be41c88620..d2f38eb3236eee482c13d7c190c0a30f727d9a43 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php
@@ -33,9 +33,8 @@ class ShortcutSetController extends ControllerBase {
    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
    */
   public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) {
-    $token = $request->query->get('token');
     $link = $request->query->get('link');
-    if (isset($token) && drupal_valid_token($token, 'shortcut-add-link') && shortcut_valid_link($link)) {
+    if (shortcut_valid_link($link)) {
       $item = menu_get_item($link);
       $title = ($item && $item['title']) ? $item['title'] : $link;
       $link = array(
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 724309c15d0e1a90be8d0ddacc5e7a2787d6c056..5579d55b75372220ce999f8bd3a6a588382ad344 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -451,14 +451,15 @@ function shortcut_preprocess_page(&$variables) {
     $link_mode = isset($mlid) ? "remove" : "add";
 
     if ($link_mode == "add") {
-      $query['token'] = drupal_get_token('shortcut-add-link');
       $link_text = shortcut_set_switch_access() ? t('Add to %shortcut_set shortcuts', array('%shortcut_set' => $shortcut_set->label())) : t('Add to shortcuts');
-      $link_path = 'admin/config/user-interface/shortcut/manage/' . $shortcut_set->id() . '/add-link-inline';
+      $route_name = 'shortcut.link_add_inline';
+      $route_parameters = array('shortcut_set' => $shortcut_set->id());
     }
     else {
       $query['mlid'] = $mlid;
       $link_text = shortcut_set_switch_access() ? t('Remove from %shortcut_set shortcuts', array('%shortcut_set' => $shortcut_set->label())) : t('Remove from shortcuts');
-      $link_path = 'admin/config/user-interface/shortcut/link/' . $mlid . '/delete';
+      $route_name = 'shortcut.link_delete';
+      $route_parameters = array('menu_link' => $mlid);
     }
 
     if (theme_get_setting('shortcut_module_link')) {
@@ -471,7 +472,8 @@ function shortcut_preprocess_page(&$variables) {
         '#prefix' => '<div class="add-or-remove-shortcuts ' . $link_mode . '-shortcut">',
         '#type' => 'link',
         '#title' => '<span class="icon">'. t('Add or remove shortcut') .'</span><span class="text">' . $link_text . '</span>',
-        '#href' => $link_path,
+        '#route_name' => $route_name,
+        '#route_parameters' => $route_parameters,
         '#options' => array('query' => $query, 'html' => TRUE),
         '#suffix' => '</div>',
       );
diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml
index 73decdd9a0aa3e56425b619942b43c55b56b91ae..34dbac181dab1a4326f84dd0886e9322727d6e86 100644
--- a/core/modules/shortcut/shortcut.routing.yml
+++ b/core/modules/shortcut/shortcut.routing.yml
@@ -40,6 +40,7 @@ shortcut.link_add_inline:
     _controller: 'Drupal\shortcut\Controller\ShortcutSetController::addShortcutLinkInline'
   requirements:
     _entity_access: 'shortcut_set.update'
+    _csrf_token: 'shortcut-add-link'
 
 shortcut.set_customize:
   path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}'
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 0000000000000000000000000000000000000000..8c1066944b04265cab65ca73ade9da89fd9ffa59
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Access\CsrfAccessCheckTest.
+ */
+
+namespace Drupal\Tests\Core\Access;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Drupal\Core\Access\CsrfAccessCheck;
+use Drupal\Core\Access\AccessInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the CSRF access checker..
+ *
+ * @group Drupal
+ * @group Access
+ *
+ * @see \Drupal\Core\Access\CsrfAccessCheck
+ */
+class CsrfAccessCheckTest extends UnitTestCase {
+
+  /**
+   * The mock CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $csrfToken;
+
+  /**
+   * The access checker.
+   *
+   * @var \Drupal\Core\Access\CsrfAccessCheck
+   */
+  protected $accessCheck;
+
+  /**
+   * The mock user account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $account;
+
+  public static function getInfo() {
+    return array(
+      'name' => '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->account = $this->getMock('Drupal\Core\Session\AccountInterface');
+
+    $this->accessCheck = new CsrfAccessCheck($this->csrfToken);
+  }
+
+  /**
+   * Tests CsrfAccessCheck::appliesTo().
+   */
+  public function testAppliesTo() {
+    $this->assertEquals($this->accessCheck->appliesTo(), array('_csrf_token'), 'Access checker returned the expected appliesTo() array.');
+  }
+
+  /**
+   * Tests the access() method with a valid token.
+   */
+  public function testAccessTokenPass() {
+    $this->csrfToken->expects($this->once())
+      ->method('validate')
+      ->with('test_query', 'test')
+      ->will($this->returnValue(TRUE));
+
+    $route = new Route('', array(), array('_csrf_token' => 'test'));
+    $request = new Request(array(
+      'token' => 'test_query',
+    ));
+    // Set the _controller_request flag so tokens are validated.
+    $request->attributes->set('_controller_request', TRUE);
+
+    $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request, $this->account));
+  }
+
+  /**
+   * Tests the access() method with an invalid token.
+   */
+  public function testAccessTokenFail() {
+    $this->csrfToken->expects($this->once())
+      ->method('validate')
+      ->with('test_query', 'test')
+      ->will($this->returnValue(FALSE));
+
+    $route = new Route('', array(), array('_csrf_token' => 'test'));
+    $request = new Request(array(
+      'token' => 'test_query',
+    ));
+    // Set the _controller_request flag so tokens are validated.
+    $request->attributes->set('_controller_request', TRUE);
+
+    $this->assertSame(AccessInterface::KILL, $this->accessCheck->access($route, $request, $this->account));
+  }
+
+  /**
+   * Tests the access() method with no _controller_request attribute set.
+   *
+   * This will default to the 'ANY' access conjuction.
+   */
+  public function testAccessTokenMissAny() {
+    $this->csrfToken->expects($this->never())
+      ->method('validate');
+
+    $route = new Route('', array(), array('_csrf_token' => 'test'));
+    $request = new Request(array(
+      'token' => 'test_query',
+    ));
+
+    $this->assertSame(AccessInterface::DENY, $this->accessCheck->access($route, $request, $this->account));
+  }
+
+  /**
+   * Tests the access() method with no _controller_request attribute set.
+   *
+   * This will use the 'ALL' access conjuction.
+   */
+  public function testAccessTokenMissAll() {
+    $this->csrfToken->expects($this->never())
+      ->method('validate');
+
+    $route = new Route('', array(), array('_csrf_token' => 'test'), array('_access_mode' => 'ALL'));
+    $request = new Request(array(
+      'token' => 'test_query',
+    ));
+
+    $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request, $this->account));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..37eec23ab95d94c92e4f09047e4b41a420956ee5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Access\RouteProcessorCsrfTest.
+ */
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\Access\RouteProcessorCsrf;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests the CSRF route processor.
+ *
+ * @see Drupal
+ * @see Routing
+ *
+ * @see \Drupal\Core\Access\RouteProcessorCsrf
+ */
+class RouteProcessorCsrfTest extends UnitTestCase {
+
+  /**
+   * The mock CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $csrfToken;
+
+  /**
+   * The route processor.
+   *
+   * @var \Drupal\Core\Access\RouteProcessorCsrf
+   */
+  protected $processor;
+
+  public static function getInfo() {
+    return array(
+      'name' => '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->processor = new RouteProcessorCsrf($this->csrfToken);
+  }
+
+  /**
+ * Tests the processOutbound() method with no _csrf_token route requirement.
+ */
+  public function testProcessOutboundNoRequirement() {
+    $this->csrfToken->expects($this->never())
+      ->method('get');
+
+    $route = new Route('');
+    $parameters = array();
+
+    $this->processor->processOutbound($route, $parameters);
+    // No parameters should be added to the parameters array.
+    $this->assertEmpty($parameters);
+  }
+
+  /**
+   * Tests the processOutbound() method with a _csrf_token route requirement.
+   */
+  public function testProcessOutbound() {
+    $this->csrfToken->expects($this->once())
+      ->method('get')
+      ->with('test')
+      ->will($this->returnValue('test_token'));
+
+    $route = new Route('', array(), array('_csrf_token' => 'test'));
+    $parameters = array();
+
+    $this->processor->processOutbound($route, $parameters);
+    // 'token' should be added to the parameters array.
+    $this->assertArrayHasKey('token', $parameters);
+    $this->assertSame($parameters['token'], 'test_token');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d23ca6b57aac5291cfc640a6bfa3ff607d55742
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\RouteProcessor\RouteProcessorManagerTest.
+ */
+
+namespace Drupal\Tests\Core\RouteProcessor;
+
+use Drupal\Core\RouteProcessor\RouteProcessorManager;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests the RouteProcessorManager class.
+ *
+ * @group Drupal
+ * @group Routing
+ *
+ * @see \Drupal\Core\RouteProcessor\RouteProcessorManager
+ */
+class RouteProcessorManagerTest extends UnitTestCase {
+
+  /**
+   * The route processor manager.
+   *
+   * @var \Drupal\Core\RouteProcessor\RouteProcessorManager
+   */
+  protected $processorManager;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Route processor manager',
+      'description' => 'Tests the RouteProcessorManager class.',
+      'group' => 'Routing',
+    );
+  }
+
+  public function setUp() {
+    $this->processorManager = new RouteProcessorManager();
+  }
+
+  /**
+   * Tests the Route process manager functionality.
+   */
+  public function testRouteProcessorManager() {
+    $route = new Route('');
+    $parameters = array('test' => 'test');
+
+    $processors = array(
+      10 => $this->getMockProcessor($route, $parameters),
+      5 => $this->getMockProcessor($route, $parameters),
+      0 => $this->getMockProcessor($route, $parameters),
+    );
+
+    // Add the processors in reverse order.
+    foreach ($processors as $priority => $processor) {
+      $this->processorManager->addOutbound($processor, $priority);
+    }
+
+    $this->processorManager->processOutbound($route, $parameters);
+  }
+
+  /**
+   * Returns a mock Route processor object.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The Route to use in mock with() expectation.
+   * @param array $parameters
+   *   The parameters to use in mock with() expectation.
+   *
+   * @return \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected function getMockProcessor($route, $parameters) {
+    $processor = $this->getMock('Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface');
+    $processor->expects($this->once())
+      ->method('processOutbound')
+      ->with($route, $parameters);
+
+    return $processor;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
index 75d051dea8bb57d047c0891de7c00580dd0703d1..d1c59c93fd38321017fa9cf8e46fbbff92e4b640 100644
--- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
@@ -8,21 +8,15 @@
 namespace Drupal\Tests\Core\Routing;
 
 use Drupal\Component\Utility\Settings;
-use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Config\NullStorage;
-use Drupal\Core\Config\Context\ConfigContextFactory;
 use Drupal\Core\PathProcessor\PathProcessorAlias;
 use Drupal\Core\PathProcessor\PathProcessorManager;
-use Symfony\Component\EventDispatcher\EventDispatcher;
+use Drupal\Core\Routing\UrlGenerator;
+use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Routing\RouteCollection;
 use Symfony\Component\Routing\RequestContext;
 
-use Drupal\Tests\UnitTestCase;
-
-use Drupal\Core\Routing\UrlGenerator;
-
 /**
  * Basic tests for the Route.
  *
@@ -44,8 +38,20 @@ class UrlGeneratorTest extends UnitTestCase {
    */
   protected $generatorMixedMode;
 
+  /**
+   * The alias manager.
+   *
+   * @var \Drupal\Core\Path\AliasManager|\PHPUnit_Framework_MockObject_MockObject
+   */
   protected $aliasManager;
 
+  /**
+   * The mock route processor manager.
+   *
+   * @var \Drupal\Core\RouteProcessor\RouteProcessorManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeProcessorManager;
+
   public static function getInfo() {
     return array(
       'name' => 'UrlGenerator',
@@ -121,14 +127,18 @@ function setUp() {
     $processor_manager = new PathProcessorManager();
     $processor_manager->addOutbound($processor, 1000);
 
+    $this->routeProcessorManager = $this->getMockBuilder('Drupal\Core\RouteProcessor\RouteProcessorManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
     $config_factory_stub = $this->getConfigFactoryStub(array('system.filter' => array('protocols' => array('http', 'https'))));
 
-    $generator = new UrlGenerator($provider, $processor_manager, $config_factory_stub, new Settings(array()));
+    $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, new Settings(array()));
     $generator->setContext($context);
     $this->generator = $generator;
 
     // Second generator for mixed-mode sessions.
-    $generator = new UrlGenerator($provider, $processor_manager, $config_factory_stub, new Settings(array('mixed_mode_sessions' => TRUE)));
+    $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, new Settings(array('mixed_mode_sessions' => TRUE)));
     $generator->setContext($context);
     $this->generatorMixedMode = $generator;
   }
@@ -163,6 +173,11 @@ public function testAliasGeneration() {
     $url = $this->generator->generate('test_1');
     $this->assertEquals('/hello/world', $url);
 
+    $this->routeProcessorManager->expects($this->once())
+      ->method('processOutbound')
+      ->with($this->anything());
+
+
     // Check that the two generate methods return the same result.
     $url_from_route = $this->generator->generateFromRoute('test_1');
     $this->assertEquals($url_from_route, $url);
@@ -177,6 +192,9 @@ public function testAliasGeneration() {
   public function testGetPathFromRouteWithSubdirectory() {
     $this->generator->setBasePath('/test-base-path');
 
+    $this->routeProcessorManager->expects($this->never())
+      ->method('processOutbound');
+
     $path = $this->generator->getPathFromRoute('test_1');
     $this->assertEquals('test/one', $path);
   }
@@ -188,6 +206,10 @@ public function testAliasGenerationWithParameters() {
     $url = $this->generator->generate('test_2', array('narf' => '5'));
     $this->assertEquals('/goodbye/cruel/world', $url);
 
+    $this->routeProcessorManager->expects($this->exactly(3))
+      ->method('processOutbound')
+      ->with($this->anything());
+
     $options = array('fragment' => 'top');
     // Extra parameters should appear in the query string.
     $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options);
@@ -209,6 +231,9 @@ public function testAliasGenerationWithParameters() {
    * Tests URL generation from route with trailing start and end slashes.
    */
   public function testGetPathFromRouteTrailing() {
+    $this->routeProcessorManager->expects($this->never())
+      ->method('processOutbound');
+
     $path = $this->generator->getPathFromRoute('test_3');
     $this->assertEquals($path, 'test/two');
   }
@@ -220,6 +245,10 @@ public function testAbsoluteURLGeneration() {
     $url = $this->generator->generate('test_1', array(), TRUE);
     $this->assertEquals('http://localhost/hello/world', $url);
 
+    $this->routeProcessorManager->expects($this->once())
+      ->method('processOutbound')
+      ->with($this->anything());
+
     $options = array('absolute' => TRUE, 'fragment' => 'top');
     // Extra parameters should appear in the query string.
     $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options);
@@ -233,6 +262,10 @@ public function testUrlGenerationWithHttpsRequirement() {
     $url = $this->generator->generate('test_4', array(), TRUE);
     $this->assertEquals('https://localhost/test/four', $url);
 
+    $this->routeProcessorManager->expects($this->exactly(2))
+      ->method('processOutbound')
+      ->with($this->anything());
+
     $options = array('absolute' => TRUE, 'https' => TRUE);
     // Mixed-mode sessions are not enabled, so the https option is ignored.
     $url = $this->generator->generateFromRoute('test_1', array(), $options);