New proposed commit message:
Issue #2493033 by Berdir, Wim Leers, lauriii, Fabianx, effulgentsia, dawehner, catch, msonnabaum, Crell, webchick: Make 'user.permissions' a required cache context
Problem/Motivation
The SmartCache issue promises to bring a huge performance boost. But, it's not without caveats. This issue is about significantly reducing one of those caveats. Please read on.
Quoting myself in #2429617-62: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!):
We had a (very long) discussion about the security implications of this issue at DrupalCon LA with Mark Sonnabaum, Wim Leers, Crell, dawehner, webchick, Alex Pott and fgm.
The problem
If a piece of code (an access result, a function building a render array …) forgets to add a certain cache context, it's possible for information disclosure to happen: if something should only be accessible for users with role A, and the
user.roles
cache context is missing, then if a user with role A first accesses the content, and then a user with role B accesses it, then the user with role B will be able to see the content.[…]
The solution
- Document security considerations for custom/contrib modules (and custom especially): those who don't have the resources to either do the necessary QA nor write the necessary test coverage should opt out by setting
max-age = 0
.- …
This issue is about implementing that requirement.
Proposed resolution
Make 'user.permissions' a required cache context
Remaining tasks
Make tests green.
User interface changes
None.
API changes
None. Solely a default configuration change.
Comment | File | Size | Author |
---|---|---|---|
#41 | interdiff.txt | 3.7 KB | lauriii |
#41 | make_user_permissions-2493033-41.patch | 27.28 KB | lauriii |
#39 | user_permissions_required_cache_context-2493033-38.patch | 28.63 KB | Wim Leers |
#31 | user_permissions_required_cache_context-2493033-31.patch | 27.01 KB | Berdir |
#27 | user_permissions_required_cache_context-2493033-27-interdiff.txt | 546 bytes | Berdir |
Comments
Comment #1
Wim LeersComment #3
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedWe should keep that still I think.
We discussed that a site builder could possibly remove user.permissions, this makes that impossible.
So hmm, not sure, but I think core should treat itself as if that cache context was not present by default ...
Comment #4
Wim LeersShould fix a bunch of fails.
I suspect I'm unable to reproduce many of these failures though.
Comment #5
dawehnerShould we document it here, why we are doing it?
Comment #6
Wim LeersYes.
Comment #8
Wim Leers@effulgentsia made an interesting remark: what if we don't make
user.permissions
a required cache context (which affects all render cache items and SmartCache items), but just a cache context that a required SmartCache cache context?I.e. only do it at the higher level (SmartCache) and not at every level (render cache).
Thoughts?
Comment #9
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedI still like the hardened security of the user.permissions, but we also see some issues with that obviously, e.g. contrib could forget to add it (as its already there), which pretty much makes ripping it out more difficult.
As Daniels concerns have been mainly about caching controllers (where you least expect it), I think that is a good compromise.
We could still allow this as an optional security hardening, but that can be decided on a site-by-site basis and maybe we need to make it easy to enable.
It is pretty clear we need to run tests without it being a required cache context - as else we don't find missing cache contexts.
Comment #10
Wim LeersWell, no, the point is that it is okay for controllers to forget about it.
But, if they're doing render caching as well, then they have to think about adding the right cache contexts. And this makes sense: if you're doing simple stuff, we've got you covered (SmartCache adds the
user.permissions
cache context). If you're building an expensive/complex/advanced render array and you're doing render caching on it, well, then you better make sure it is correct.So: we protect novice developers from shooting themselves in the foot, but still expect advanced developers to do things correctly.
Pinging catch, Alex Pott and dawehner to get their thoughts on #8 as well.
Comment #11
dawehnerI think the problem not just exists for smart cache but also every other render cache entry. From block caching, over views row caching etc.
I still hope we start with thinking of security first in our mind
Comment #12
BerdirAgreed, adding user.permissions always seems like a good "security first, performance second" decision.
A while ago, we removed user.permission from rendered entities by default, making modules that add data responsible to add that. That resulted security issues in a few contrib modules that we're working on, for example simplenews and flag, which are adding links in hook_node/user_view() and similar places, based on permissions.
The majority of users on most sites will have a the same set of permissions, and the number of variations are usually not *that* big.
If not everywhere, then maybe at least in places that are very dynamic/commonly altered, like entities, blocks, views?
Comment #13
Wim LeersBut those are the majority of things that are render cached. So, to KISS, we might then as well just do it always, and hence make it a required cache context.
Thanks for all the feedback. Seems like sufficient signs to me that we should pursue the original direction, and finish the patch in #4.
Comment #14
catchI think it makes sense to have user permissions as a required cache context.
I'm trying to figure out if there are good cases to allow people to opt-out. All I can think of is expensive database queries with no variation, or external http requests to web services. For web service requests you might have to limit how many calls are done - and four roles could mean four times as many cache misses (i.e. even if there are 20,000 cache hits, there might be 400 cache misses instead of 100).
However for cases like that, I think it's OK to say you have to cache the expensive/rate-limited stuff yourself, with whatever cache key is appropriate, then that cache item is good for however many different render misses.
The only other case would be a site where everything depending on permissions has been moved into post render cache / placeholders. Or a block that is cached per-page that has no variation. For these it would be nice to offer an opt-out, but I can't think of a way to do that except for an #exclude_cache_contexts key which is quite ugly.
Comment #15
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedSo my biggest problem with the patch atm. is that like in #14 the user.permissions is not easily excludable again.
I think what would be nice is to have the default be different from the user.permissions 'real' one. So an alias so to speak.
user.permissions.secure_by_default?
Then if both user.permissions and user.permissions.secure_by_default are present it would be collapsed to user.permissions, but if user.permissions or user or whatever is not present, then it would just be user.permissions.secure_by_default be present and hence be secure.
That kind of distinction is important IMHO as we still want to ensure that things correctly declare what they vary on - despite it being a default context.
Comment #16
Wim LeersI fear that the proposal in #15 in trying to simplify the future actually complicates the present.
Comment #17
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedYes, however it has several advantages:
- e.g. msonnabaum having 20 roles (with distinct permissions hashes) could for the secure_by_default cache context, decide to use a custom cache context service, which distinguishes just:
4 categories (super-admin, admin, end-user, anon)
instead of 20 using a custom site specific algorithm.
He could still use user.permissions to automatically placeholder e.g. and leave 20 partitions for the normal user.permissions.
--
So I think it still gives us the best out of both worlds.
Comment #18
catchI noticed that as user 1 looking at user/1 (although page doesn't matter), we do 5 cache redirects in HEAD:
This is because the block entity render cache misses the cache contexts added by the plugins or what they render:
user.roles.authenticated cache context is due to the logout menu link I think - that's fine.
All the rest we'd save redirecting if we had user.permissions by default - which is 4 cache gets in HEAD.
So small performance improvement here too.
Comment #19
Berdir@catch: Yes. Similar, related note that we could optimize: At least one of the redirects I saw only had user.roles:anonymous in addition to user.permissions. At least in our implementation in core, we could optimize that away.
I think we found the reason for those test fails. The patch didn't have the change for default.services.yml which is used for new test environments.
Comment #21
BerdirThis should fix most of the fails.
Comment #22
effulgentsia CreditAttribution: effulgentsia at Acquia commented+1 to this issue. However:
Contrib could make it possible via the same kind of service decorator around hasPermission() as is being done in #2351015: URL generation does not bubble cache contexts around URL generation. But it can only do so if there's a service to decorate. Therefore: #2526514: Make User(Session)::hasPermission() a service.
Comment #24
BerdirThis makes the new default cache tags/contexts a bit more flexible/intelligent. Adding user to ViewsUnitTestBase uncovered the broken create() method in the RolesRid plugin. Fixed that. We should consider enabling a lot more modules in PluginInstanceTest so we are testin more of those plugins, in a follow-up.
Comment #25
BerdirWrong patch.
Comment #27
BerdirThis should fix that last fail.
Comment #28
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commented#22:
The making that pluggable move is great!
My concern is that people forget to use:
- $access->cachePerPermissions(), because it works anyway.
I guess that trade-off is okay and if someone has 16-20 roles they might have the development resources to either replace the PermissionsCacheContext with a simpler version (and hence also the permissions hash) or be able to audit the site for things wrong when that required context is removed.
Comment #29
Wim LeersAwesome!
Comment #31
BerdirEasy reroll, conflicted with #2516802: FilterProcessResult->merge() results in PHP warning: Missing argument 1 for FilterProcessResult::__construct() which also added user.permissions in FilterApiTest.
Comment #32
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedI am very happy about #31, but we should at least discuss the implications of #28 and #15 (even if the secure_by_default is not a good idea, so ignore that part).
Comment #33
effulgentsia CreditAttribution: effulgentsia at Acquia commentedFor me, #22 is the answer to that. In other words, we make it ok for people to leave off the 'user.permissions' context in their render arrays / access results / etc., because if a contrib module wants to remove 'user.permissions' as a required context, it can decorate the service proposed in that other issue, such that any call to ->hasPermission() adds the context to the render stack, just like we're doing in core for URL generation.
Comment #34
Wim Leers#33++
I do agree with Fabian that it is sad/unfortunate that this will mean people will forget to be explicit. But that's the point: we want to ease the transition. We want to protect against the most common mistakes. Which does mean being less strict about permissions, by always adding that cache context. Which effectively means
. That's precisely what we don't want to do, but we can't have both.I think it's a very good compromise to not require developers to specify the
user.permissions
cache context. They'll start to think about cacheability already anyway, but we'll have their back for the most common case.In D9, we'll be able to stop holding their hands. Hopefully, we'll also have better APIs in D9 that still require you to think about cacheability, but don't require you to manually merge cacheability of dependencies.
But for D8, this seems the sanest and safest way forward. Besides, D8 sites that care about getting this right will only need to provide simple patches, with simple test coverage, to D8 contrib modules. Which means it'll be absolutely doable to build sites that don't have
user.permissions
as a required cache context.Given that, moving back to RTBC.
Comment #35
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedRTBC + 1, I just wanted to have it discussed :).
It however means we need to be extra-careful in core, too ;) - though I guess that is acceptable.
Proposed commit message:
Note:
msonnabaum, Crell, webchick have participated and contributed to that in the discussion around this that led to the decision to do this at DrupalCon LA.
Comment #36
Wim Leers+1 for the proposed commit message.
Yes, but we can fix them in normal/major issues over the lifetime of D8 too.
Comment #38
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedComment #39
Wim LeersRebased. Included interdiff shows conflict resolution.
(Straight reroll.)
Comment #41
lauriiiFixed failing tests
Comment #42
effulgentsia CreditAttribution: effulgentsia at Acquia commentedInterdiff looks good, so back to RTBC.
Comment #43
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedRTBC + 1 (again)
--
New proposed commit message:
msonnabaum, Crell, webchick have participated in the discussion at LA around this issue, where we already discussed IRL a lot of the pro's and con's.
Comment #44
catchShould be a user with different permissions no?
Shouldn't this message be 'block not served from cache'?
I'd fix these on commit, but I want to double check I'm not reading the test change incorrectly here.
Comment #45
Wim LeersYes.
Indeed; copy/paste error :(
Splendid!
Comment #47
catchCommitted/pushed to 8.0.x, thanks!