Problem/Motivation
(This is split off from #2351015: URL generation does not bubble cache contexts, because the scope of that issue is more about the security implications rather than the cacheability.)
Links with CSRF tokens break cacheability.
(Generally speaking, anything with CSRF tokens currently breaks cacheability.)
Blocking
Proposed resolution
- Replace the
CacheableMetadata
parameter of all the outbound path/route processing methods and URL/link generation methods withBubbleableMetadata
(which extendsCacheableMetadata
!), this allows outbound path/route processors to attach placeholders. - The CSRF route processor can then render a placeholder instead, and have that placeholder be replaced at the very last moment.
Note: it also opens the door for route/path processors to attach asset libraries. Which means that e.g. contrib can implement an alternative CSRF route processor that adds the CSRF token using JavaScript!
Remaining tasks
- Review.
- In a follow-up issue: apply the same technique to CSRF tokens elsewhere (particularly in forms) — #2463567: Push CSRF tokens for forms to placeholders + #lazy_builder.
User interface changes
None.
API changes
Surprisingly: none!
We replace the CacheableMetadata
parameter of all the outbound path/route processing methods and URL/link generation methods with BubbleableMetadata
, which extends CacheableMetadata
:) The best way to describe this is API addition.
Data model changes
None.
Beta phase evaluation
Issue category | Task because an improvement. |
---|---|
Issue priority | Major because not strictly release blocking, though without this, the effectiveness of #2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) is greatly diminished, as any link with a CSRF token causes SmartCache to not cache that response. |
Prioritized changes | The main goal of this issue is performance/cacheability. |
Disruption | No disruption. |
Comment | File | Size | Author |
---|---|---|---|
#37 | cacheable_csrf_links-2512132-37.patch | 95.53 KB | Wim Leers |
Comments
Comment #1
Wim LeersCacheableMetadata
parameter of all the outbound path/route processing methods and URL/link generation methods withBubbleableMetadata
(which extendsCacheableMetadata
!), this allows outbound path/route processors to attach placeholders.Comment #2
Wim Leers(Rolled on top of #2450993-98: Rendered Cache Metadata created during the main controller request gets lost, hopefully also applies to HEAD.)
Comment #3
Wim LeersNote:
cacheable_csrf_links-2512132-1-bubbleablemetadata_only.patch
was created by looking at the entire diff for #2335661: Outbound path & route processors must specify cacheability metadata (commit7342df6
), and just updating every change that introducedCacheableMetadata
toBubbleableMetadata
. That's all that 87.73 KB does.The 3.36 KB part of the patch is the actually interesting one.
Comment #4
Wim LeersFinal remark.
And this is a simple bugfix.
Calling
BubbleableMetadata::createFromObject()
falls through toCacheableMetadata::createFromObject()
in HEAD. Which means that attachments were being lost.Comment #5
Wim LeersWe should still add a
session
cache context.Comment #6
effulgentsia CreditAttribution: effulgentsia at Acquia commentedWhere's the per-session part reflected in the cache context?
Comment #7
effulgentsia CreditAttribution: effulgentsia at Acquia commentedLOL. #6 is x-post with #5.
Comment #8
Fabianx CreditAttribution: Fabianx as a volunteer commentedI think for now we are okay to use max-age=0, else this will be cached per user and we get bugs.
Comment #10
Wim LeersMigrating a bit from the IS of #2351015: URL generation does not bubble cache contexts.
Comment #11
Wim LeersComment #12
Wim LeersTests fixed.
The patch in #1 that does "the simple stuff" has already passed, so not reuploading it. It is unchanged. It still is the base for the "key changes" one.
The interdiff is for the "key changes" patch, which is also updated, and therefore the overall combined patch should be passing now :)
EDIT: when reviewing, see #1 for details about how this patch is split up to simplify the review process.
Comment #13
Fabianx CreditAttribution: Fabianx for Acquia commentedRTBC, looks great. Needs beta eval, doc updates for the new cache context, change record updates, etc. as usual ...
Comment #14
Wim LeersBeta evaluation is already here :)
Comment #15
Wim LeersUpdated the affected CR: https://www.drupal.org/node/2480761/revisions/view/8431479/8601712.
Comment #16
Wim LeersClarified the API addition.
Comment #17
Fabianx CreditAttribution: Fabianx for Acquia commentedDo we need a change record update for the introduction of the cache hierarchy?
Comment #18
Wim LeersNo, just updating https://www.drupal.org/developing/api/8/cache/contexts should suffice.
Comment #20
Wim Leers#2509300: Path alias UI allows node/1 and /node/1 as system path then fatals broke this.
Rebased.
Comment #21
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThe improvement to RouteProcessorCsrf is fantastic: yay for placeholders!
But, the issue summary says there's no API change and no disruption, but sadly that's not true, because PHP does not implement contravariance for parameter types (PHP--). Therefore, the changing of the processOutbound() signature for OutboundRouteProcessorInterface and OutboundPathProcessorInterface will result in every contrib module that implements one of those breaking fatally until they change their signature to match.
Options:
instanceof
check on the $cacheable_metadata parameter that they get. So, for example, RouteProcessorCsrf could do that check, and if true, could render a placeholder, but if not true, could set max-age to 0.I see benefits to both options (#1 results in simpler code due to needing fewer
if
statements, but #2 is less disruptive to already ported contrib modules), so setting to "needs review" for feedback from others.Comment #22
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI'm open to being convinced otherwise, but after thinking about it some more, currently I'd prefer #21.2, because the
instanceof
check could be forAttachmentsInterface
. So, every outbound processor gets aCacheableMetadata
object, whose purpose is clear (tell us how this processor affects cacheability), and the very few that want to optimize their caching by using a placeholder can do so in anif
statement that checks whether the $cacheable_metadata object they were given supports attachments (i.e., implements AttachmentsInterface).Comment #23
Wim Leers+1
It is in that light that this patch is able to be RTBC: outbound route/path processors are extremely rare.
Comment #24
effulgentsia CreditAttribution: effulgentsia at Acquia commentedDo you have data to back that up?
Comment #25
Fabianx CreditAttribution: Fabianx for Acquia commented#22:
I am torn between "Changing the API again is really bad" and "This just got in, there can't be that many users, yet."
I also think it could be a major "WTF!":
The interface says "CacheableMetadata", but you "should" pass in BubbleableMetadata, but no-one except some docs tell you.
And we have to do this for the whole D8 cycle.
My main concern is:
When we just ship with CacheableMetadata and then we have a contrib module, which uses the functionality of opting in via AttachmentsInterface, but then there is another contrib module, which displays links, but just uses CacheableMetadata, then those two clash.
So I think we should bite the bullet and do the API change.
Comment #26
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI don't see what the WTF would be. The only caller of processOutbound() that creates the object to pass in is UrlGenerator. So, really, only if you're swapping out UrlGenerator do you need to make a decision of what to pass to processOutbound(). And if we want to, we can make UrlGenerator's protected methods processPath() and processRoute() typehint their last parameter to GeneratedUrl, which I think will provide adequate guidance to people subclassing or replacing UrlGenerator.
I think we can also add docs to OutboundRouteProcessorInterface and OutboundPathProcessorInterface that for best cache optimization, callers are encouraged to pass in a $cacheable_metadata object that also implements AttachmentsInterface, and that processors that could benefit from using placeholders or other attachments should do so within an
if ($cacheable_metadata instanceof AttachmentsInterface)
statement.So for example, suppose a contrib module does swap out UrlGenerator and ends up invoking processOutbound() with a $cacheable_metadata object that doesn't implement AttachmentsInterface. Nothing functionally breaks. All that happens is rendered content (e.g., blocks, pages, node views, menus) that contain links to CSRF-protected routes are not cached when they otherwise could have been. Similar kind of progressive degradation would happen for contrib processors. That's not really a clash though, is it?
I'm trying to apply the reasoning from https://www.drupal.org/contribute/core/beta-changes#bc to this issue, and that says that BC is needed unless:
I don't think the BC I'm proposing is either infeasible or would introduce significant technical debt. The $cacheable_metadata parameter was added to processOutbound() on May 9th, so I think there is a possible argument that 7 weeks is still sufficiently new, but it's not true that it's not used anywhere yet. For example, Secure Login added it to its signature on May 14th.
Comment #27
catch#24 - anecdotally I only ever come across outbound path processors in custom code - usually where there are extremely specific requirements and for whatever reason people didn't want to use aliases.
Note we've just changed the API for inbound path processors in #2509300: Path alias UI allows node/1 and /node/1 as system path then fatals, and it's even rarer to have an outbound path processor without an accompanying inbound path processor. While the nature of the change here is different, I don't think there will be much real term impact in someone having to change some code that they otherwise wouldn't have if we break BC.
Comment #28
Wim LeersThey just had to update their signature. As they would have to here. That's all.
Combined with the anecdotal experience in #27, I think it's fair to say that BC impact will be super minimal.
Comment #29
effulgentsia CreditAttribution: effulgentsia at Acquia commentedGreat, sounds like we're ok with the signature change. In that case, should we change it all the way to GeneratedUrl? That way, if in the future we decide that GeneratedUrl should implement some other capability too, that we want outbound processors to be able to impact, we can add it with no future signature change needed?
Also:
Where do they vary per user? And also, is it possible for different users to have the same session?
Comment #30
catchI also wondered about the second question in #29. Shouldn't do any harm but looks unnecessary/confusing.
Comment #31
Wim LeersIt sounds like you mean something else than this:
… but I'm not sure what then. That already ensures that if we add other types of cacheability metadata, that they automatically become available here too.
Hrm… good point. It doesn't seem it does. That's just what I've read dozens of times, so I assumed it to be true. But it indeed looks like it's just per-session.
You mean masquerade-module-like?
Comment #32
Wim LeersUgh, I thought this was at RTBC…
First, straight rebase.
Comment #33
Wim LeersAll questions that were the reason for un-RTBCing have been answered. The patch is current again, with only trivial changes. Therefore, re-RTBC'ing.
P.S.: I think it'd be great to land this before #2351015: URL generation does not bubble cache contexts, actually, because otherwise we may need to change APIs again.
Comment #35
Wim LeersIn addressing #29 (i.e. only keeping the CSRF route processor's
session
cache context, removing theuser
cache context), I forgot to update two integration tests. Hence the failures. Here's the fix.Comment #36
catchCouple of proper questions, couple of small nits.
If we have a #lazy_builder, why does the placeholder need to be cached by session? Aren't we adding this just so that it's not only cacheable by session?
In which case, do we need this at all? And then, do we even need the parameter change here to make CSRF links cacheable? I'd expect the #lazy_builder/placeholder to be it. The parameter change looks fine to me for the js use case or similar things adding js tied to links, just not at all clear why it's necessary here - makes the patch a lot larger for what appears to be a 20 line change.
bubbleeable
object dependency?
Comment #37
Wim Leers(Note that this means that
SmartCache
can still cache the overall HTML, because SmartCache renders the HTML without replacing the placeholders, precisely because they are so dynamic. It's only the final (post-SmartCache) HTML response that varies persession
.)Yes, in theory we don't need the
session
cache context. 99% of things will work just fine. But reverse proxies will then not know that the response they're passing on is actuallysession
-specific. So they'll cache it and also send it to other users.CacheableMetadata::createFromObject()
. (Feel free to change on commit, but if you do, can you do it in both places?)Discussed 1 + 2 with catch; turns out he just was pointing out that
#cache[contexts] = [session]
should be in the#lazy_builder
callback, that would've been much clearer. Totally makes sense. Done.Comment #38
Fabianx CreditAttribution: Fabianx for Acquia commented#36: It is a judgment call, but I think it is okay to cache the placeholders per session, but maybe we should use a max-age=86400 to have them be expired some time?
We could definitely remove that, too.
But I think it might even make sense to have the e.g. logout link be ESI'd in per user (and cached in Varnish) in the future.
Comment #39
Fabianx CreditAttribution: Fabianx for Acquia commented#37: X-POST, yes that makes sense too and would bubble up correctly, so could also be cached by ESI, so best out of both worlds ...
I also forget we don't have set 'keys' so no need for max-age.
=> Nvm, on #38, RTBC + 1
Comment #40
catchCommitted/pushed to 8.0.x, thanks!
Comment #43
dawehnerToo bad, this broke other stuff, see #2630920: _csrf_token is broken due to cacheability metadata integration, results in rendered links without valid CSRF tokens