Problem

Starting with Drupal 10.3 and 11 the user.logout is protected with a CSRF token. Now the user logout link of the `account` menu has a token query parameter. With that token, the link just works. If the token is wrong, a confirmation forms is shown as fallback action.

It turns out that the user logout link provided by the rest-menu-items API contains a *wrong* token. It never passes validation. Open the absolute URL of the user.logout link.

Expected: You are logged out.
Actual: Log out does not work, the confirmation-form fallback is shown.

Note that the confirmation form is not helpful in decoupled environments, what makes this more severe.

Steps to reproduce

Launch D11, e.g. via the "try it" button at https://www.drupal.org/project/lupus_decoupled. Access the API via the URL `api/menu_items/account`.

First analysis

I verified session and seed is correct when token is generated and validated. However, somehow the token provided with the rest_menu_item menu link ends up being wrong. At some point there is also a right token generated on the menu-api request, but it does not end-up in the URL being generated.

Comments

fago created an issue. See original summary.

fago’s picture

I tracked it down: the problem is the wrong token, is not a token, it's a placeholder, which ought to be replaced by the renderer.

See the logic of RouteProcessorCsrf:

      // Adding this to the parameters means it will get merged into the query
      // string when the route is compiled.
      if (!$bubbleable_metadata) {
        $parameters['token'] = $this->csrfToken->get($path);
      }
      else {
        // Generate a placeholder and a render array to replace it.
        $placeholder = Crypt::hashBase64($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]]);
      }

\Drupal\Core\Render\MetadataBubblingUrlGenerator::generateFromRoute() seems to activate this logic always, even when the Url is generated with $url->toString(FALSE).

fago’s picture

to reproduce, run this with drush php

> \Drupal\Core\Url::fromUri('internal:/user/logout')->toString();
= "/user/logout?token=fzL0Ox4jS6qafdt6gzGzjWGb_hsR6kJ8L8E0D4hC5Mo"

compare the token with the right token, it's wrong, it's the placeholder value. So seems this is triggered by a core bug.

However, additionally rest_menu_items has a bug since it calls > \Drupal\Core\Url::fromUri('internal:/user/logout')->toString(TRUE); but throws the resulting bubbleablemetadata away. By throwing it away, the placeholders won't be replaced.

Generally, I think the module should re-use the pre-existing $url object provided by the menu API and not re-create it with the uri. When done so, we'd not trigger bugs like this one.

fago’s picture

so, re-using the pre-exiting url object does not solve it either. Anyway, Drupal core generates the token with a placeholder, but since we are not rendering obviously placeholders are not replaced. :-( I wonder how this is solved for the drupal core menu linkset api.

fago’s picture

tested it. it does not - it faces the same problem. Let's open a core issue!

--> #3485174: Menu APIs provide invalid CSRF tokens

fago’s picture

Status: Active » Fixed

I can confirm this is fixed now. With the fix in core, this works, so let's mark this as fixed also!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.