UserRolesCacheContext can lead to poisoned cache returns for user 1

Created on 27 March 2024, 9 months ago

Problem/Motivation

This is a public follow-up to a security issue as it was deemed low risk in the private security issue.
11/25 (Moderately Critical) AC:Complex/A:Admin/CI:Some/II:Some/E:Theoretical/TD:Default

Steps to reproduce

  • Create a block that shows your user roles, obviously vary it by user.roles
  • With only the Admin role, visit a page with said block as UID 1, see that it lists the Admin role
  • Assign another role (e.g. Editor) to UID 1
  • Visit the same page as before and see that the block lists the Admin, but not Editor role

This also works the other way around, where roles are revoked and the more permissive result is still shown, which is why it was reported as a security issue. In some modules, this is leading to unexpected results because said modules have code that relies on what roles you have rather than what permissions you have. For user 1 this is leading to confusing outcomes.

Proposed resolution

Remove the special user 1 string from the following code:

  public function getContext($role = NULL) {
    // User 1 does not actually have any special behavior for roles; this is
    // added as additional security and backwards compatibility protection for
    // SA-CORE-2015-002.
    // @todo Remove in Drupal 9.0.0.
    if ($this->user->id() == 1) {
      return 'is-super-user';
    }
    if ($role === NULL) {
      return implode(',', $this->user->getRoles());
    }
    else {
      return (in_array($role, $this->user->getRoles()) ? 'true' : 'false');
    }
  }

Especially now that we have/are about to switch to access policies, which will no longer hard-code UID 1's special behavior but rather make it a service that you can opt out of.

We've always said not to use user.roles when you should be using user.permissions, but we have to acknowledge that there might be some code out there that still did this. However, said code will remain functional (yet a security risk) but it will no longer exhibit the buggy behavior described here when it comes to user 1.

When we work on πŸ“Œ Add a container parameter that can remove the special behavior of UID#1 Fixed once πŸ“Œ Implement the new access policy API Needs work has landed, we can run all tests with UID 1's special powers turned off, at which point we can rest assured core should no longer hard-code any special UID 1 exceptions like was done here.

Remaining tasks

  • Remove the special string from the cache context
  • Perhaps update or remove RenderCacheTest.php

User interface changes

None

API changes

UID 1 no longer gets a special value from the user.roles cache context

Data model changes

None

Release notes snippet

UID 1 no longer gets a special value from the user.roles cache context

πŸ› Bug report
Status

Active

Version

11.0 πŸ”₯

Component
CacheΒ  β†’

Last updated 1 day ago

Created by

πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

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

Merge Requests

Comments & Activities

  • Issue created by @kristiaanvandeneynde
  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Some snippets from the previous discussion:

    A band-aid solution would be to merge in 'is-super-user' with the roles rather than returning a special string. This will kill caching between UID1 and other users with the same role, but at least it will not lead to stale cache entries for UID 1.

    If we do decide to go with a band-aid, we should still remove this once we fix πŸ“Œ Add a container parameter that can remove the special behavior of UID#1 Fixed

    Instead of 'is-super-user' can UID 1 ensure the administrator role name is always included?

    That could again lead to collisions where UID1 without the admin role warms the cache in a way that is then shown to someone who really has the admin role.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Update IS that core/tests/Drupal/KernelTests/Core/Render/RenderCacheTest.php will need to be removed

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Updating IS: We've had two security issues reported already that boil down to this bug. It's time to rip the band-aid off.

    Let's simply remove the incorrect return value and put out a CR and release note mention. If you're still using user.roles to determine access, which you shouldn't, then the only affected person by this change will be UID1 or people who share the same roles as UID1. And only if you had access logic riding on it. Blocks such as described in the IS will actually start working properly now.

  • πŸ‡¨πŸ‡¦Canada deviantintegral

    Here's some replication steps I found for this:

    1. Install Drupal 11.0.5 with the Standard profile.
    2. Log in as user 1.
    3. Edit `services.yml` and set security.enable_super_user: false.
    4. Clear caches to rebuild the container.
    5. Remove the administrator role from user 1.
    6. Note user 1 still has access to administrator functions.

    Or, from the CLI if you have a Drupal project created with ddev:

    cat > web/sites/default/services.yml <<EOD
    parameters:
      security.enable_super_user: false
    EOD
    ddev drush -y si && \
    ddev launch $(ddev drush uli) && \
    ddev drush user:role:remove administrator admin && \
    ddev launch # This browser tab will still have admin until you clear caches.
    

    Here's a screen recording of the issue.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Added a MR to see what tests have to say about this. I'd be surprised if everything goes green, but who knows.

  • Pipeline finished with Failed
    about 1 month ago
    Total: 107s
    #339853
  • Pipeline finished with Failed
    about 1 month ago
    Total: 107s
    #339852
  • Pipeline finished with Success
    about 1 month ago
    Total: 601s
    #339856
  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    All green, well I'll be damned.

  • πŸ‡³πŸ‡±Netherlands bbrala Netherlands

    Love the remove in Drupal 9 comment there hehe.

    Don't see how this would break anything removal is clean. Lets fix this leftover.

  • πŸ‡ΊπŸ‡ΈUnited States benjifisher Boston area
  • πŸ‡¬πŸ‡§United Kingdom catch
  • πŸ‡¬πŸ‡§United Kingdom longwave UK
  • πŸ‡¬πŸ‡§United Kingdom mcdruid πŸ‡¬πŸ‡§πŸ‡ͺπŸ‡Ί
  • πŸ‡¬πŸ‡§United Kingdom catch
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10
  • πŸ‡¬πŸ‡§United Kingdom longwave UK
  • πŸ‡¬πŸ‡§United Kingdom mcdruid πŸ‡¬πŸ‡§πŸ‡ͺπŸ‡Ί
  • πŸ‡ΊπŸ‡ΈUnited States xjm

    Adding credit for security team members who discussed the private issue related to this during the fortnightly core security triage as well as a duplicate of it.

    We agree with the public handling, and simply removing this brittle code that is incorrectly coupled to the old special behavior of user 1 seems the best course.

  • πŸ‡ΊπŸ‡ΈUnited States drumm NY, US
  • πŸ‡ΊπŸ‡ΈUnited States xjm
  • πŸ‡ΊπŸ‡ΈUnited States xjm

    Out of curiosity, I grepped to see anywhere else that is-super-user is interacted with like this and found ./core/tests/Drupal/Tests/Core/Session/SuperUserAccessPolicyTest.php, which makes sense.

    Meanwhile. The IS says that core/tests/Drupal/KernelTests/Core/Render/RenderCacheTest.php should be removed. However, the MR does not remove it. Should it be removed? I'm a little confused -- that's an awfully generic test name for the scope of this issue. Could we explain further?

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Okay so that test specifically tests the quirks that used to be (user.permissions) or still are (user.roles) in Drupal core's cache contexts. Given how we already removed one quirk by introducing the access policy API and we're about to remove the last quirk in this issue, that test has no reason to exist any more.

    The only thing that puzzles me is why the test still goes green after we remove the final quirk. I would expect it to go red for the user.roles cache context at least. I'll investigate this and report back, then we can remove the test and set the issue back to NR.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Okay so the reason the test still goes green shows how poorly we were testing things.

    Pre-patch:

    • $user_1 cache context value = is-super-user
    • $admin_user cache context value = authenticated,RANDOM_ADMIN_ROLE_NAME

    Post-patch:

    • $user_1 cache context value = authenticated
    • $admin_user cache context value = authenticated,RANDOM_ADMIN_ROLE_NAME

    So obviously that assert still goes green both before and after the patch.

    So RenderCacheTest::testUser1PermissionContext() only goes green because we deliberately set $usesSuperUserAccessPolicy to TRUE and added a todo there to remove said flag. This warrants a test rewrite or removal.

    Then RenderCacheTest::testUser1RolesContext() goes green both with and without the change, this is explained above.

    The funny thing is that the second test case automatically also tested for user.permissions because those cache contexts are always added. This leads to the test still going green even if we remove the quirk from the UserRolesCacheContext. Adding the following code to the test makes it go green before the removal, but red after the removal.

      public function setContainer(ContainerBuilder $container): void {
        $renderer_config = $container->getParameter('renderer.config');
        $renderer_config['required_cache_contexts'] = [];
        $container->setParameter('renderer.config', $renderer_config);
        $this->container = $container;
      }
    

    So all in all, this test was all sorts of broken and not even testing the thing it promises it was testing. Its removal is long overdue.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Removed the test. Should go green again and in my opinion is ready to ship given my explanation in #30.

  • Pipeline finished with Success
    10 days ago
    Total: 7673s
    #365523
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    πŸ“Œ [Meta] Fix all tests that rely on UID1's super user behavior Active just in case here's the META that's tracking those $usesSuperUserAccessPolicy replacements, wooo done to 2 and 1 may be in contrib now.

  • πŸ‡ΊπŸ‡ΈUnited States dww

    I read the MR changes closely, and the issue summary and comments.

    @kristiaanvandeneynde makes a compelling case that RenderCacheTest is really broken. There were already a couple of @todo comments in there about removing it. πŸ˜…

    The actual change to stop the special case context for UID 1 makes sense to me. grepping core confirms that this and core/tests/Drupal/Tests/Core/Session/SuperUserAccessPolicyTest.php are the only places that mention is-super-user. In that test, it's only used to label cases inside a data provider array, not for any actual code, assertions, etc.

    I renamed the MR to be self-documenting, but otherwise, all looked great.

    I don't see anything left to do here but commit it. RTBC++

    Thanks!
    -Derek

  • πŸ‡ΊπŸ‡ΈUnited States dww

    Oh, does this need a CR? I guess that'd be the only other possible thing to do here.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    It will be released as a security release, so I'd hope people read that even more than they would a CR.

  • πŸ‡ΊπŸ‡ΈUnited States greggles Denver, Colorado, USA

    Issues that will be packaged in a security releases and advisories are worked on in private. Working on this in public was agreed which means there will be no advisory nor security release.

    The question of whether it needs CR? Not sure personally.

Production build 0.71.5 2024