Problem/Motivation
#2351015: URL generation does not bubble cache contexts is implementing CSRF tokens as placeholders for links, but a lot of the issues can be tried out for forms, which ensures that Form caching - except for crazy #default_value's is one step nearer and the link CSRF issue will get easier.
Proposed resolution
Remaining tasks
User interface changes
API changes
| Comment | File | Size | Author |
|---|---|---|---|
| #75 | push_csrf_tokens_for-2463567-75.patch | 9.57 KB | borisson_ |
| #75 | interdiff.txt | 1.16 KB | borisson_ |
Comments
Comment #1
wim leersTaking this on.
Comment #2
wim leersSuper hacky super rough initial version.
This causes an error upon submitting a form:
We'll need to update the form building/tracking of the token in form state stuff too. But I've mostly forgotten about Form API's internals. (i.e. why it works the way it works…) Pointers are most welcome, so I don't need to spend hours re-learning FAPI.
Comment #3
wim leersComment #5
wim leersThe (majority of the) 199 failures are caused by the tricky way GET forms' input processing is handled by the form builder: their input is only processed in certain scenarios, and because Views happens to
$form['form_token']['#access'] = FALSE, Views' exposed filters (GET) form happens to not be one of the scenarios where processing happens, causing these test failures.This was @effulgentsia's clever work-around :)
Pair-programmed with @effulgentsia.
Comment #7
borisson_The remaining failures are caused because the "form_token" is in a different format than expected:
<input value="{value}" type="hidden" name="form_token" />instead of<input type="hidden" name="form_token" value="{value}">The attached patch fixes the test for QuickEditAutocompleteTermTest so it uses the actual format of the input. I'm pretty sure this is a bad way of fixing this.
There are 2 options:
- Make the regex more flexibile, so it doesn't matter in what order the attributes of the input are.
- Fix the actual input to have the expected order of attributes.
What is the preferred way forward?
Comment #9
wim leersNow doing exactly this for links: #2512132: Make CSRF links cacheable. Postponing this on that.
Comment #10
wim leersComment #11
wim leers#2512132: Make CSRF links cacheable landed. We can apply the same solution.
Comment #12
borisson_The implementation in the patch in #7 was using #post_render and that didn't work anymore.
I've changed this to use a
#lazy_buildercallback, this is till WIP and doesn't work yet.Comment #13
dawehnerJust use cre32, this is what we use in other places ...
Do we have a better name for this?
Comment #14
borisson_#13
hash('sha1', ...), so for consistency I also went for that here, I can change this if you want.preRenderSetPlaceholderAsValueAttached patch doesn't functionally change anything compared to #12, there's only the method rename.
Comment #16
wim leers#14.1: Yes, let's change that to
crc32(). I discussed that with dawehner;crc32()is sufficient here; we should also update all other cases (i.e. the issue you linked to and\Drupal\Core\Render\Renderer::createPlaceholder()— if you open an issue for the latter, I'll RTBC it :)).@borisson_: Thanks for taking this one on! When I saw it fly by just now, I immediately though of you, since it's related to that other issue. But clearly you're alredy working on it :P
Comment #18
borisson_This should a bunch of the current failures.
Comment #20
borisson_I removed too much code in the previous patch.
Comment #21
wim leerss/#post_render_cache/#lazy_builder/
s/render/renders/
s/csrf/CSRF/
Just the
sessioncache context should be sufficient.This could use some comments, much like those in #2504139: Blocks containing a form include the form action in the cache, so they always submit to the first URL the form was viewed at.
Comment #23
wim leersThe test failures are simple: just missing the
sessioncache context :)Comment #24
borisson_This patch contains fixes for #21 (interdiff_comments.txt) and fixes for the tests.
Comment #25
fabianx commented$this->csrfToken->get($path);
$placeholder = 'form_token_placeholder_' . crc32($form_id);
e.g. we need to 'namespace' our placeholders somewhat.
Are we sure we want to get() that value, I guess it does not matter as its invalid anyway, but just asking.
Might need a little comment.
Does this clash with HTML5 placeholder?
Here are the comments.
I can't yet see how this can work in form validation, but I guess I will see once this is green :).
Great work!
Comment #27
borisson_#25
\Drupal\Core\Render\Element\Token::preRenderSetPlaceholderAsValuethis placeholder gets copied to the value.This also fixes the failures in QuickEditLoadingTest
Comment #28
borisson_Comment #30
borisson_Comment #32
borisson_This makes sure that a session is set so the test can use it, this fixes the last test failure in
CommentDefaultFormatterCacheTagsTestComment #33
wim leersIncomplete docblock.
Nit: s/id/ID/
Needs docblock.
Comment #34
borisson_#33
Comment #35
wim leers1. The one I quoted! Note how it just lists the params without documentation.
This one is also incomplete for the same reason.
(Yes, I know, our docs standards are quite silly for cases like these.)
We still have the "needs tests" issue tag. Do we really need tests for this though? We have a gazillion implicit tests anyway, right? So I think we're fine in that regard.
Comment #36
borisson_Updated the docblocks of both methods.
---
I agree, I think we have sufficient implicit tests for this.
Comment #37
wim leersThese docblocks are wrong.
We always have a blank line between the last
@paramand@return.And we never put the documentation for a parameter on the same line, always on a new line.
See the many thousands of examples in the rest of core :)
Hrm, the
$pathparameter does not actually make sense.Hrm, we're still using the CSRF token generator here. That seems wrong. (I know it is a remnant from my early patch in #5, but that doesn't make it right :))
@Fabianx already asked for its removal in #25.3, you already did that in #27, but because it caused test failures, you restored it in #30.
But that just means we need to figure out why #27 failed so badly.
This is IMHO the biggest blocker.
This is the clever work-around invented by @effulgentsia, as I wrote in #5. We need to document why and how this works, because I sure don't understand it (anymore).
Comment #38
borisson_#37
$path, renamed it, see .3This needed to be the CSRF token because previously, this ran after the #lazy_builder and otherwise the
CsrfTokenGenerator::validatecall returned false.It is now removed, as well as the
'#placeholder', so the confusion in #25.4 doesn't happen anymore.Locally, all forms still work and can be submitted, I'd like to see what happens with the test-suite.
Comment #40
borisson_So, the current failures are all entity listings that fail, and as far as I can see, they are failing because of the filters that are on these listings.
If I debug
$tokenand$this->computeToken($seed, $value)in\Drupal\Core\Access\CsrfTokenGenerator::validatethen it shows that they aren't the same.Example:
So, the
#lazy_builderhasn't ran yet for the filter form. This is what needs to be solved for the failures to be resolved.Comment #41
xano@borisson_ and I went through the code this morning and we discovered a few interesting things:
ExposedFormPluginBase::renderExposedForm()andDisplayPluginBase::elementPreRender()build complete forms using the form builder, while their return values should be embeddable forms.This means there are multiple form token elements in the final form, all of which say their value is stored inFormStateInterface::getValue('form_token'). As form tokens are based on form IDs, all elements have different token values, and as such only one of them can possibly match the token in the form state, causing the other elements to fail validation.Comment #42
borisson_Attached a reroll, also changed the placeholder to be attached to the form_token element, not the parent form build array.
Comment #44
xanoThe reason for the test failures is that GET forms unnecessarily received form token elements, but GET forms are validated before rendering/submission, so the CSRF token was never set, because lazy builders are part of the rendering process.
Because GET forms are immutable, which means they don't need CSRF protection, we can safely prevent CSRF token elements from being added to such forms.
Comment #46
borisson_I had introduced the changes to QuickEditLoadingTest and QuickEditAutocompleteTermTest earlier in this patch, reverted these back to the state of HEAD and they pass again.
Comment #47
borisson_The current approach removes CSRF tokens for get forms completely. Is this a security concern?
If it is, we might be able to also fix the the failures of #42 by reverting the changes in #44 and setting #token to false for exposed filter forms.
Comment #48
wim leersI pinged @effulgentsia for review.
Comment #51
borisson_Patch no longer applied, this one does, let's see what kind of failures we're getting now.
Comment #53
borisson_Comment #55
borisson_Comment #57
borisson_Comment #59
borisson_FormBuilderTestwill still fail, looking into fixing that.Comment #61
borisson_I looked into fixing the remaining failure in
FormBuilderTest, but I can't figure it out yet, I'll have another look tomorrow.Comment #62
borisson_Rerolled the patch and fixes the
FormBuilderTest.Comment #63
wim leersI think we should split this off into a separate issue, and block this issue on that new issue.
That's an API change in line with #2502785: Remove support for $form_state->setCached() for GET requests and fully sensible, but this issue really is only about improving how CSRF tokens are rendered. This change is something else entirely.
Also, if we do this, then we should also remove all manual calls that set
$form['#token'] = FALSEin GET forms. E.g.\Drupal\search\Form\SearchBlockForm::buildForm()does this.Comment #64
borisson_postponing on #2571995: GET forms shouldn't have CSRF tokens by default.
Comment #65
borisson_Attached patch removes the code quoted in #63.
This will not work before #2571995: GET forms shouldn't have CSRF tokens by default is in.
Comment #66
borisson_Comment #67
borisson_Moving 'needs security review' tag to #2571995: GET forms shouldn't have CSRF tokens by default.
Comment #68
borisson_Now that #2571995: GET forms shouldn't have CSRF tokens by default is in, we can finish this.
Comment #69
borisson_Attached patch is the same as in #65.
Comment #72
borisson_Rebased.
Comment #73
wim leersThis patch presents a big step forward; it opens the door towards cacheable forms. But let's keep marking forms uncacheable for now, because that requires a broader discussion.
So:
max-age=0, but doing so requires further discussion.Comment #74
effulgentsia commentedPatch looks great, but a couple of nits:
I don't understand this comment. "could've been different" from what?
Elsewhere we use hash('crc32b') instead of crc32(), possibly due to the portability warning in http://php.net/manual/en/function.crc32.php. Let's switch to that here as well.
Comment #75
borisson_Fixes both comments of #74.
Comment #78
wim leersPIFR was having MySQL troubles it seems. Re-testing. DrupalCI came back green already though. Back to RTBC.
Comment #80
effulgentsia commentedThanks! Pushed to 8.0.x.