By default user 1 has no role assigned (except for 'authenticated user'). The cache keys created through cache_context.user.permissions
have no special handling for the user 1 (only the roles are considered) and therefore are valid both for the super user and authenticated users.
This may cause unexpected behavior.
An example (non critical issue) on a clean drupal installation:
- Login as user 1
- Place the Administration Menu block on the first sidebar
- Clear cache
- Logout
- Login as authenticated user
- The Administration Menu is still there..
but potentially the problems created by this issue can be much more serious
Comment | File | Size | Author |
---|---|---|---|
#66 | permissionshash_superuser-2417895-66.patch | 888 bytes | David_Rothstein |
#61 | interdiff.txt | 888 bytes | David_Rothstein |
#61 | permissionshash_superuser-2417895-61.patch | 13.89 KB | David_Rothstein |
#58 | permissionshash_superuser-2417895-58-interdiff.txt | 1.03 KB | Berdir |
#58 | permissionshash_superuser-2417895-58.patch | 13.88 KB | Berdir |
Comments
Comment #1
willzyx CreditAttribution: willzyx commentedComment #2
willzyx CreditAttribution: willzyx commentedComment #3
Wim LeersGreat catch! This could lead to information disclosure, or worse.
This is a side-effect of Drupal being role-based… but with this one exception.
Comment #4
Wim LeersComment #5
catchThis is a Drupal 7 issue too, so I've posted it to the private tracker.
Unpublishing this issue for now - I'll let willzyx know via contact form.
Comment #6
Wim LeersRe-publishing.
This has been fixed in Drupal 7: https://www.drupal.org/SA-CORE-2015-002
Comment #7
Wim LeersHere's a cleaner solution that I wrote while working on the D7 fix. May no longer apply, just uploading verbatim. Dates back from March 21, 2015.
In D8, role IDs are strings (e.g.
'anonymous'
), so therer1
for UID 1 makes sense.Comment #9
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedFor reference, the Drupal 7 patch (which also contains tests that could be ported to Drupal 8) can be found here:
http://cgit.drupalcode.org/drupal/commit/?id=5cb79b4b217e9aa315d61284398...
(see the includes/common.inc and modules/simpletest/tests/common.test parts of the commit)
Credit for that Drupal 7 fix is:
David_Rothstein, Wim Leers, willzyx
Comment #10
dawehnerTalking about those kind of permissions, don't we also have a problem now with the admin roles being implemented outside of permissions?
Comment #11
Wim LeersUpdating issue; we now have
\Drupal\Core\Cache\Context\AccountPermissionsCacheContext
for permission checking.UserRolesCacheContext
is not for permission checking, only for presence-of-role checking.Until #2428703: Add a 'user.permissions' cache context (was: "Should cache contexts be able to associate a cache tag?"), Drupal 8 was awkwardly and erroneously assuming that "permissions" === "roles".
Comment #12
Wim Leers#10: No, because
User
andUserSession
(i.e. theAccountInterface
implementations) have this:i.e. they only special-case UID 1.
It is then
RoleStorage::isPermissionInRoles()
that takes advantage of the::isAdmin()
stuff you're referring to:In other words: a role being an "admin" role has zero consequences for the "roles a user has", but does have consequences when looking at permissions. But that's the job of the
AccountPermissionsCacheContext
, so that is what we need to fix.That being said, we could add a
user.is_admin
cache context for "admin user or not" variations. This is similar to theuser.is_super_user
(\Drupal\Core\Cache\Context\IsSuperUserCacheContext
) cache context that we have for "UID 1 or not" variations.Comment #13
Wim LeersThe real bug lies not in
AccountPermissionsCacheContex
, but inPermissionsHashGenerator
: that should've been special-casing the super user (user 1) all along, but it has not. The current implementation there is wrong, because the permissions for the super user are NOT determined by the union of the permissions of the roles of user 1: it simply gets all permissions.So, fixed
PermissionsHashGenerator
instead, and updated its test coverage.Its unit test was also named wrongly:
PermissionsHashTest
instead ofPermissionsHashGeneratorTest
— fixed that too.Comment #14
Wim LeersAdding a clarifying comment, based on a discussion with catch in IRC.
Comment #15
tim.plunkettIt's nice to have one tag to find all of the public critical security issues, and this is on 5 of them already.
Comment #16
dawehnerI think we should better add also an integration test for this particular issue, you know, just to be safe in the future.
Comment #17
Wim LeersFair.
Comment #18
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedYes, I think the tests mentioned in #9 should be forward-ported.
Comment #19
BerdirNot quite sure how to do that, honestly.
Yes, we could try to port those tests as close as possible, but we actually converted that test to unit tests in 8.x. And since this is now pretty nicely decoupled from the render system, it seems kind of backwards to go back to kernel-integration-something tests for the render system. It doesn't make sense to cover that in the unit test for the render system because as I mentioned, the actual code is completely decoupled.
So.. that kind of leaves only a full-blown web tests with either a test block implementation or a real example somewhere that uses permission access inside a block/entity.
Thoughts?
Comment #20
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedThere is already Drupal\system\Tests\Render for rendering-related non-unit tests; I think it can just go there.
The Drupal 7 tests for this issue seem like about the right level to me; they are high-level enough that they ensure the cache doesn't leak between users when rendering is performed in an actual Drupal environment, but low-level enough that they aren't tied to a specific rendering situation.
The attached patch forward-ports those tests, but it isn't working yet. I think the code that tries to override the current request method and force it to be a GET request isn't taking effect correctly. So each of these patches should currently have the same two test failures.
Comment #22
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedCame back and took another look, and that turned out to just be due to some random caching Symfony does of the REQUEST_METHOD...
That's fixed, but the tests still aren't passing. However now the failures are the ones you'd expect to see if the bug isn't actually fixed. I haven't looked into it enough to see if that's real, or if there's something still wrong with the test.
Comment #23
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedSome more minor fixes, but this won't change anything mentioned in the previous comment.
Comment #29
willzyx CreditAttribution: willzyx commentedManually tested path in #23 and it seems to fix the original issue.
Test are failing because you are using user.roles instead of user.permissions cache context
user.roles cache context has not a special handling for user 1. If someone uses it inappropriately, we could have a information disclosure issues. We should think if add a special handling for user 1 also in user.roles is needed
Comment #30
BerdirFine, let's do that.
The problem with your test is that you used the user.roles context. That one *is* the same for user 1 and correctly so. The one that should be used is user.permissions. user.roles should only be used when roles are checked explicitly. Then user 1 doesn't have anything special.
I also converted it to a kernel test and removed the request method stuff... I'm 99.9% sure that can never be anything but GET. This isn't a superglobal like in D7.. the request object is manually created in KernelTestBase::setUp(), it's always the same.
This fails/passes as expected locally.
Comment #31
Berdir@willzyx: Nice crosspost, good thing you didn't already fix it yourself :)
I thought about special casing user.roles but I don't think we need that.. user.roles has several other flaws that would bite you if used incorrectly, for example, it doesn't detect permission changes in roles, so if you would grant a new permission, you'd still be using the same cache context and would get a cached response..
Also, slightly related issue that I opened yesterday: #2509478: PermissionsHashGenerator should have a static cache
Comment #33
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedAnd in Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter::viewElements():
I did some brief testing with the comment form (and with this patch applied) and couldn't reproduce an actual vulnerability with user 1 in the above cases, but I think it's really only a coincidence if you can't...
Comment #34
Berdir1. That usage might be wrong. user.permissions was introduced not too long ago, because user.roles had problems. As mentioned, it has other problems too, so using it is just wrong anyway, even if we would add another check to that one as well. See https://www.drupal.org/node/2459373 for the change record and #2428703: Add a 'user.permissions' cache context (was: "Should cache contexts be able to associate a cache tag?") for the issue that explains why user.permissions exists.
2. Being faster is the biggest reason I think. Since we're just doing API calls, I don't think there's a reason to use WebTestBase, it's not that different then. The request for web tests is AFAIK also always GET, it's created in prepareRequestForGenerator().
Comment #35
Wim LeersAgreed with Berdir's reasoning about the
user.roles
cache context. See a longer explanation in my comments #11, #12 and #13.#33/#34.1: indeed: that one is wrong. It was missed during the conversion, i.e. it was missed in #2428703: Add a 'user.permissions' cache context (was: "Should cache contexts be able to associate a cache tag?").
RTBC'ing #30.
Comment #36
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedSo given the history (this was changed nearly six months after Drupal 8 entered beta) and the fact that core still has some cases that use user.roles to handle permissions (I guess there should be a separate issue to fix that?) I think we want the same protection for user.roles as user.permissions. There isn't really a good reason for user 1 to share any role-based or permission-based cache with other users anyway.
So basically I think the test should change to do a foreach() over user.roles and user.permissions (or something similar), so that it tests both cases separately... and then fix both :)
When running this as a webtest via the SimpleTest UI, it needed to be forced to GET or the test won't pass. Probably not needed for the testbot, though, since that runs via the command line.
Comment #44
catchI don't think adding the user 1 check to user.roles is helpful. If you're using that for permissions checks, then you already have a potential information disclosure issue when your permissions change. Also when reviewing both against each other, if they both special case user 1, this implies that user/1 behaviour should be different when it isn't, so could even misdirect people to using user.roles inappropriately.
Is there an existing core issue for the bad user.roles usage?
Comment #45
Wim LeersI looked at all remaining
user.roles
usages:BookNavigationBlock
: wrong, is being fixed in #2483181: Make book navigation block cacheable.CommentDefaultFormatter
: wrong, needed bothuser.permissions
anduser.roles
, but can be updated to be more granular (useuser.roles:authenticated
, and only add it a level deeper in the if-branching)CommentForm
: wrong, needed bothuser.permissions
(always) anduser.roles
, but can be updated to be more granular (useuser.roles:authenticated
, and only add it a level deeper in the if-branching)history.module
: used correctlyLoginStatusCheck
: used correctly\Drupal\user\Plugin\views\access\Role
: used correctlyRoleAccessCheck
: used correctlyOpening an issue for fixing points 2 and 3 now.
Comment #46
Wim LeersHere we go, includes a patch: #2510034: Remove one useless cache context in CommentDefaultFormatter.
Comment #47
Wim LeersGiven #44 + Berdir's + my comments, and the fact that the wrong usages are being fixed, and patches exist: moving back to RTBC.
Comment #48
catchI've bee quite involved with this one, so I won't commit it - just to give time for David to come back or a second opinion from another committer.
Comment #49
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedI commented earlier on the closed issue that if the permissions change problem is considered a security hole we should deal with it too. But I didn't think it was really a security issue; if it only grants you access to things you previously had access to (but for longer than the site administrator intended) then it just seems like a regular cache-invalidation bug to me.
This issue meanwhile is an actual security issue. I think it's too late in the Drupal 8 cycle to assume people will change their code in response to every change notice, so given the history I think it's important to apply the security protection to both cases. (And a code comment could be added to the user.roles case explaining why this is done, so it doesn't introduce confusion for anyone reading the code.) I don't see any harm in adding this, and I think it's the only way to fully solve the security issue - so I don't think this patch should be committed in its current form.
Comment #50
alexpottI agree with @David_Rothstein I think being cautious makes sense.
Comment #51
catchOn the basis that we added the permissions cache context quite recently, then doing this as a bc layer I can see the argument for.
We should add a @todo to remove that for 9.x though - by then modules (and core!) should be using the proper cache contexts all the time.
Comment #52
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedI also agree and am bold to set back to needs work.
While we introduced permissions, we still support user.roles and it is the closest that comes to mind when one had used DRUPAL_CACHE_PER_ROLE.
The one more bucket used should not hurt.
Comment #53
BerdirI still think this is a bad idea :)
Yes, using the right cache contexts is not easy, and being cautious and on the safe side is a good thing. That's what #2493033: Make 'user.permissions' a required cache context* is about, which i think we should really do. This I consider to be more confusing than helpful.
But, if that's whats' needed to get it in, here's a patch for it.
While writing this, I actually noticed a much bigger problem, which would have been a "fun" security issue. The previous patch returned 'is-super-user', unhashed. But this hash is also used client-side, so anyone would have been able to set their local hash to 'is-super-user'. Improved this. Interesting that nobody noticed this :)
* With that issue, it would actually no longer be possible to write a failing patch for the user.roles test method.
Comment #54
Fabianx CreditAttribution: Fabianx for Drupal Association commentedRTBC, lets get this in - this provides parity with D7. We can remove uid 1 in D9 ;).
Comment #55
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedThe changes to the test look good to me. Reading through #2493033: Make 'user.permissions' a required cache context I think I agree; that issue might be the best way to get the tests here to pass.
Yikes. From what I can tell, core itself is only using this client-side for cache invalidation, though. In other words, setting your local hash to 'is-super-user' wouldn't actually do anything to get the server to send you the super-user's data, would it? Still seems worth hashing though, I agree.
I am not sure if the intention is to allow contrib modules to use those client-side hashes to actually request data from the server... if so, I would have questions about whether the hashing here is secure enough (since it doesn't have any built-in expiration; once you know it, it's essentially good for the lifetime of the site). But that's a preexisting issue.
---
Otherwise looks good - I basically agree with RTBC (almost cross-posted). But it's missing the comment explaining why we do this (and the @todo suggested by catch in #51)...
Comment #56
Fabianx CreditAttribution: Fabianx for Drupal Association commentedHm, thanks David (#55). Sorry, I missed those two tasks ...
Comment #57
Fabianx CreditAttribution: Fabianx for Drupal Association commented( tagging - sorry for the noise )
Comment #58
BerdirSomething like this?
Comment #59
Fabianx CreditAttribution: Fabianx for Drupal Association commentedYup, looks great to me! :)
Comment #60
Wim Leers/me hides in shame, despite the total lack of an actual vulnerability.
Correct. This is why there is indeed nothing vulnerable about this.
#58's comment additions look excellent.
Comment #61
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commented1. What's up with the last line - looks like it was supposed to be deleted?
2. What's "BC compatibility"? Does it stand for "backwards compatibility compatibility"? :)
3. Grammar isn't really right.
I could probably just leave this RTBC since I only changed the code comment, but I made enough changes that I'm putting this back for review; I'm sure someone will re-RTBC it soon.
Comment #62
Fabianx CreditAttribution: Fabianx for Drupal Association commentedComment #63
alexpottThis issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 8d80b7a and pushed to 8.0.x. Thanks!
Comment #64
alexpottComment #66
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedLooks like there was some cross-posting above. Reuploading the interdiff from #61 as an actual patch. @Fabianx already RTBC'd this in #62.
Comment #67
alexpottCommitted 51656b3 and pushed to 8.0.x. Thanks!