Wrong user logout CSRF token

Created on 31 October 2024, 23 days ago

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.

🐛 Bug report
Status

Active

Version

3.0

Component

Code

Created by

🇦🇹Austria fago Vienna

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @fago
  • 🇦🇹Austria fago Vienna

    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).

  • 🇦🇹Austria fago Vienna

    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.

  • 🇦🇹Austria fago Vienna

    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.

  • 🇦🇹Austria fago Vienna

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

Production build 0.71.5 2024