2

How can I get a link with the correct csrf token for an internal path? Here is what I have:

in mymodule.routing.yml

mymodule.test:
  path: '/mymodule-test'
  defaults:
    _content: 'Drupal\mymodule\TestController::handleTest'
    _title: 'Handles test'
  requirements:
    _permission: 'access content'
    _csrf_token: 'TRUE'

building the link:

//$url = Url::fromRoute('mymodule.test');
//$settings['url'] = $url->toString();
$settings['url']=Drupal::url('mymodule.test');

the url contains the token in the query string as I expect, but it doesn't validate if I try to visit the link. When I edit CsrfTokenGenerator and dump out the token and the expected token, they do not match. All I can come up with is that the token is being generated for the page that I'm currently viewing rather than the route that I'm generating the url for.

Am I missing something? any ideas are appreciated!

6
  • CSRF tokens are used only in forms, to my (very limited knowledge in this subject) where it makes sense. So if you need CSRF token for some of your pages then you also need to process/validate the value on your own. Either during page request via EventSubscriber or in block/part of the page where you need validation.
    – user21641
    Commented Sep 26, 2015 at 7:49
  • Are you using mymodule.test as the argument to url or are you using mymodule-test?
    – greggles
    Commented Sep 26, 2015 at 13:20
  • 1
    @IvanJaros CSRF tokens are used also in the menu router to protect menu callbacks that are not form submissions
    – Trey
    Commented Sep 26, 2015 at 21:48
  • 1
    @greggles mymodule.test it expects a route in D8 , not a path
    – Trey
    Commented Sep 26, 2015 at 21:48
  • 1
    BTW, CSRF tokens are used in both menu callbacks and forms to protect against CSRF. You have to explicitly add them to menu paths that you want to protect, but they are added to all forms by default.
    – greggles
    Commented Sep 28, 2015 at 15:17

3 Answers 3

4

Routes that need CSRF tokens have the _csrf_token: 'TRUE' route option. That's how URLs pointing to those routes automatically get a CSRF token.

So we need to look at \Drupal\Core\Access\RouteProcessorCsrf:

public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
  if ($route->hasRequirement('_csrf_token')) {
    …
    if (!$bubbleable_metadata) {
      $parameters['token'] = $this->csrfToken->get($path);
    }
    else {
      // Generate a placeholder and a render array to replace it.
      $placeholder = hash('sha1', $path);
      $placeholder_render_array = [
        '#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
      ];

      // Instead of setting an actual CSRF token as the query string, we set
      // the placeholder, which will be replaced at the very last moment. This
      // ensures links with CSRF tokens don't break cacheability.
      $parameters['token'] = $placeholder;
      $bubbleable_metadata->addAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]);
    }
  }
}

So there we can see that if $bubbleable_metadata is NULL, then we generate the CSRF token right away. $bubbleable_metadata being NULL signals to the CSRF route processor that the URL is being generated in a context where no cacheability metadata and attachments can be bubbled (e.g. when generating a URL to be sent in an e-mail). The downside is that the HTML containing this URL will then be uncacheable (well, very poorly cacheable), because the CSRF token is depends on the session.

That's why in the other case, where $bubbleable_metadata is a BubbleableMetadata instance, that signals to the CSRF route processor that the URL is being generated in a context where cacheability metadata and attachments can be bubbled (e.g. when generating a URL to be rendered in some HTML). So there we generate a placeholder token that will be replaced with the CSRF token, and specify a "placeholder" attachment that will ensure that placeholder token is replaced with a CSRF token at the last possible moment. Consequently, the HTML that this URL ends up in is still perfectly cacheable, because the URL is cacheable.

Now that problem has been confirmed. If this happens in a preprocess function, as the following one, then you end up with $variables['url'] = GeneratedUrl(…). Twig ought to bubble the cacheability metadata and attachment metadata in that object, but it does NOT, in all versions of Drupal 8 before 8.1.2. We're fixing this for Drupal 8.1.2 at https://www.drupal.org/node/2575519.

function hook_preprocess_HOOK(&$variables) {
  $variables['url'] = \Drupal::url('mymodule.test');
}
3
  • I understand this... the question is how to get that link... you mention (e.g. when generating a URL to be sent in an e-mail), assuming that's my use case, how exactly do I generate that link? If you've looked at my original post, or the comments in the other answer, you can see that I've actually been trying to get it to do exactly this, are you suggesting I call this method directly?
    – Trey
    Commented Oct 9, 2015 at 19:15
  • Updated my response. There's an actual bug part, see drupal.org/node/2575519.
    – Wim Leers
    Commented May 5, 2016 at 10:40
  • This has been committed on the 8.2.x branch. I doesn't seem it will be back ported to the 8.1.x branch.
    – avpaderno
    Commented May 17, 2016 at 5:06
2

This is probably the same problem as token but that was recently fixed: Browse available tokens not shown

The problem is that your token isn't actually a token, it's just a placeholder for a token that will be replaced later. However, the action to do that replacement (called lazy builder) is not being attached/bubbled up properly.

Where are you calling this? Try a solution similar to what's in the issue if you have a link, or try to get the cacheability metadata from the return value of toString(TRUE) and attach that to some render array:

$url =  Url::fromRoute('mymodule.test')->toString(TRUE);
BubbleableMetadata::createFromRenderArray($element)
  ->merge($url)
  ->applyTo($element);

$variables['url'] = $url->getGeneratedUrl();

As far as I understand, this is supposed to just work, but it apparently doesn't under some conditions. We'll need to see if we can fix that in core.

4
  • Thanks I'll get back to you later after I've had a chance to try this... I'm working trying to get the plupload module working and this was their methodology for the the upload path that gets passed to the javascript. It seemed like the right idea so I thought I'd try to fix it instead of working around it
    – Trey
    Commented Sep 26, 2015 at 21:50
  • Unfortunately this didn't work since I'm not working with a renderable array, but I definitely appreciate the direction since it is clearly related.
    – Trey
    Commented Sep 28, 2015 at 20:25
  • Yes, I fear we have a problem here for links that aren't built in/for render arrays. You'll probably have to go through a render array somehow to make this work. Try to ask in #drupal-performance or so.
    – Berdir
    Commented Sep 28, 2015 at 21:06
  • I actually tried a custom render array, and Url::toRenderArray in conjunction with your suggestion but am still stuck, thanks again, all those years on Drupal 6 and 7 and now I feel like a complete newb lol
    – Trey
    Commented Sep 28, 2015 at 21:31
0

Finally came up with a solution.

$urlBubbleable = $urlObject->toString(TRUE);
$urlRender = array(
  '#markup' => $urlBubbleable->getGeneratedUrl(),
);
BubbleableMetadata::createFromRenderArray($urlRender)
  ->merge($urlBubbleable)->applyTo($urlRender);
$url = \Drupal::service('renderer')->renderPlain($urlRender);

It's my opinion that there should be a much easier way to accomplish this. We should be able to process the URL without having to pass it through the renderer.

But, for now, this works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.