See: https://www.drupal.org/SA-CORE-2015-003
http://cgit.drupalcode.org/drupal/commit/?h=7.x&id=731dfacab8bf39918c135...
A vulnerability was discovered in Drupal's form API that could allow file upload value callbacks to run with untrusted input, due to form token validation not being performed early enough. This vulnerability could allow a malicious user to upload files to the site under another user's account.
This vulnerability is mitigated by the fact that the uploaded files would be temporary, and Drupal normally deletes temporary files automatically after 6 hours.
Credit for the D6/D7 version of this patch (the security release):
Abdullah Hussam, greggles, Wim Leers, David_Rothstein, larowlan, pwolanin
Comment | File | Size | Author |
---|---|---|---|
#52 | increment.txt | 1.42 KB | pwolanin |
#52 | 2554233-52.patch | 23.55 KB | pwolanin |
#47 | increment.txt | 5.39 KB | pwolanin |
#47 | 2554233-47.patch | 23.41 KB | pwolanin |
#44 | interdiff.txt | 1.39 KB | tim.plunkett |
Comments
Comment #2
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThis should do it.
Comment #4
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedA patch that doesn't break all forms should work better :)
Comment #5
tim.plunkettThis is sad and problematic. Hopefully it can be removed if we refactor to delegate to FormValidator.
Otherwise, we need to inject the string translation service to not depend on \Drupal.
We need to make this extensible, in this patch, not in a follow-up.
Large parts of this are duplicating stuff that's in \Drupal\Core\Form\FormValidator::validateForm(), we should address that instead of copy/pasting.
Comment #7
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedWhy wasn't it made extensible in the D7 patch?
If by "large parts" you mean those two lines of code that get the URL and set the form error, then sure, how about this?
Comment #8
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI would feel a lot better if we could put this on the FormState object instead, but that one doesn't use StringTranslationTrait either...
Comment #9
Wim LeersDon't we want a more fundamental solution in D8, rather than using the whitelist that D7 *had* to use, because anything else would mean a BC break? Or is that for a follow-up?
Comment #10
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedNo idea, I asked a similar thing in #7.1 :)
Comment #12
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedFixed the newly added test, the other two test failures are above my knowledge in this area.
Comment #13
tim.plunkettSo the reasons we did not make it extensible in the sdo issue was:
a) it would have taken longer
b) we had many options, and wanted to debate the approach in public amongst more than those working on the security issue
I meant the if() parts too, because they are very similar, but I guess they are slightly different? Hm. But yes, that change is a step in the right direction.
Could setInvalidToken and setInvalidTokenError be combined? I know they're currently on different objects... Or one could call the other
Comment #14
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI don't think we can do that because the call to
setInvalidTokenError()
fromFromValidator::validateForm()
has a check for$form_state->hasInvalidToken()
in itsif()
condition.Comment #16
catchCould we add a new empty interface for those elements to say that their value callbacks are safe to run? Then any element that meets the criteria can opt-in to being safe to run. I don't have a great name for that interface, but seems the easiest way to communicate it.
Comment #17
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThat could potentially work for the valueCallback() method of a class, but wouldn't work for a '#value_callback' override, such as color_palette_color_value().
Comment #18
Wim Leers#17: Can we require
#value_callback
s to be classes?Comment #19
catchWe could at least require them to be classes and implement that interface if they want to be treated as safe? And any other callable is unsafe by default.
Comment #20
Wim Leers#19: that was the alternative that came to mind :)
Comment #21
dawehnerCan we explain why its okay to call them?
Comment #22
effulgentsia CreditAttribution: effulgentsia at Acquia commentedSo for example, a
ValueCallbackIsSafe
interface that any class can implement, and implementing that means that that class'svalueCallback()
method (and only that one) is safe to invoke even if there are errors already flagged (such as an invalid CSRF token)? Then, what if a class subclasses such a class and overrides valueCallback() with an unsafe implementation? I don't think there's a way to unimplement an interface in a subclass.Seems like what we really want is to add some metadata to a function/method. Any reason not to do that with an annotation? One reason is we don't yet annotate functions in Drupal, only classes, but is that really a compelling reason not to start to do so?
Comment #23
larowlanCould we make the white-list a container parameter and leave it as is?
If your module has another safe value callback, implement ServiceModifierInterface and add to the white-list.
Comment #24
catchWe currently don't do any annotation parsing on runtime, only on rebuilds. So we'd either need to break that pattern (could get expensive doing it every time though?) or build a registry of safe-to-call methods. I'm not sure if those are compelling reasons not to do it but they're the concerns that immediately came to mind.
Is there any unsafe implementation other than a file upload? If so how likely is it that someone would subclass a class that's not for a file upload and add one if so?
It'd also be possible to mark the method itself as final. I've never seen this used anywhere, but PHP does allow it.
Comment #25
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI like #23. Just for completeness, some other options:
1) Call all value callbacks, and make it their responsibility to check $form_state->getErrors() prior to doing something unsafe.
2) Add a by-reference parameter to valueCallback(). E.g.
function valueCallback(&$element, $input, FormStateInterface $form_state, &$is_safe)
. Then, when FormBuilder invokes value callbacks on elements, if there are form errors, it first invokes the callback with $input=FALSE, and if that invocation results in $is_safe being set to TRUE, then it invokes it again with the user's input. Extra function call, but only during relatively rare situations.3) Create a $form_state->setValueCallbackIsSafe($value_callback) method, and let modules decide where they want to invoke that from to mark their value callback as safe.
Comment #26
effulgentsia CreditAttribution: effulgentsia at Acquia commentedIf we only cared about file uploads, then we wouldn't need to whitelist value callbacks at all, we could just empty out $_FILES and be done. color_palette_color_value() is another example of one though: it currently implements its own CSRF token validation because that predates SA-CORE-2015-003. Since that's an example, there's probably others out there somewhere.
Comment #27
Wim LeersThis was discussed on the EU criticals call, by Alex Pott, catch, larowlan, dawehner and I. The consensus was:
Comment #28
pwolanin CreditAttribution: pwolanin at Acquia commentedHere's a fix for a couple of the test fails including a small FAPI fix that might need to go back to D7.
The current code in FormBuilder::processForm() looks like the wrong/too late place for the check, since we it looks like we might call this earlier code in the method with #token being false
Comment #29
tim.plunkettI protested this going into D7 without an extensible fix, and was told that it would not go into D8 without one. In the interest of progress, I'll roll over once again...
Now that the prepareForm hunk is moved out of the $user->isAuthenticated hunk, this processForm code is unreachable. Removing it is not necessary to fix the bug, but should be done because it is now dead code. +1
The only call to setInvalidToken() is right after calling setInvalidTokenError().
Yet this second call to setInvalidTokenError() is right after a hasInvalidToken() check. I didn't dig into the code flow here, but that seems wrong somehow. As I said before, setInvalidToken() and setInvalidTokenError() have a very confusing relationship.
Comment #30
catch@timplunkett I think it's better to get it in with no extensibility than to rush the extensibility in. Gives us time to figure out between container parameter vs. interface etc. as a pure API addition. Once we add the extensibility we're stuck with it until 9.x.
Comment #31
tim.plunkettI guess #29.3 just means two calls to setInvalidTokenError, and $form_state->setErrorByName already handles duplicate errors.
So from my perspective, this is RTBC.
Comment #32
tim.plunkettNope not RTBC. Explaining why in a follow-up comment, just wanted to prevent it from being committed.
Comment #33
tim.plunkettWe likely need to expand this list, we have a ton more elements.
This in_array is not going to work anymore, since this callable can now be an array. In fact, any normal element is defined as
array($definition['class'], 'valueCallback')
right now.This means the test coverage is inadequate.
Comment #34
pwolanin CreditAttribution: pwolanin at Acquia commentedIndeed - just tested this patch with a local proxy and tampering with the form token during the POST causes all the input text to be lost.
Comment #35
pwolanin CreditAttribution: pwolanin as a volunteer commentedThis fixes the basic value callback problem. I added one more valid one and made a list in comments of those I excluded. the date ones looked a little complex for my taste, including loading config and there have been past vulns in DateTime handling like: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-0273
Still needs more tests. Possibly, new tests should be backported to 7.
Comment #36
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedSome start on more test coverage. Not getting radios to work, however.
They also seem not to work in manual testing, so possibly there is something extra going on in validation which is not reached.
Comment #37
tim.plunkettHere's a unit test.
Comment #39
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedShould we make the method public? I find unit tests of internal methods a bit suspect.
Comment #40
tim.plunkettWanting to test a protected method is not a reason to make it public.
Comment #41
dawehnerShould we call out to
$request->files = new FileBag();
?Does that mean we should implement
serialize()
/unserialize()
?I think its kinda fine to test that.
Comment #42
tim.plunkettRadio wasn't working because we didn't add it to the list.
Added 41.1
Not sure what you mean by 41.2, we document a lot of form_state properties like this, it's just to show that it's not in $form_state->getCacheableArray().
Comment #43
dawehnerAh I wasn't aware of it.
Well, my intention was to remove the first line
This is twice the same test case
Comment #44
tim.plunkettComment #45
xjmDiscussed with @webchick, @alexpott, @effulgentsia, and @catch. As a forward-port for a security advisory, this issue is definitely critical.
Comment #46
dawehnerThank you for addressing my feedback.
Comment #47
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedTurns out 8 is more annoying than expected - basically everything is extending FormElement so has a valueCallback defined by inheritance. I think I added now most all of the things. Added a few more test cases.
Also, we should include the default Drupal\Core\Render\Element\FormElement::valueCallback - which does nothing, but returns NULL so the input is taken directly.
Comment #48
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedIt feels like there could be another lingering security bug from #2380053: [core] Posting an array as value of a form element is allowed even when a string is expected (and bypasses #maxlength constraints) - first step: text fields
In 7 we have:
But here in 8 the Password type just returns NULL and doesn't use the check from textfield, and textfield doesn't check the value is scalar.
Comment #49
dawehnerDoes that mean we should have a blacklist instead? It is really not clear for me given how many things we define for core, that we don't need extensibility ... just have a look at TableSelect ... this could be easily a custom thing
can we typehint with @callable? We also need docs
Comment #50
pwolanin CreditAttribution: pwolanin as a volunteer commentedWe really want a whitelist instead of a blacklist, but possibly we could do it via an added method (which would give immediate extensibility).
Comment #51
dawehnerWell I guess this issue is about fixing core, but it feels odd to not provide a mechanism for contrib to deal with it.
Comment #52
pwolanin CreditAttribution: pwolanin as a volunteer commented@dawehner - see earlier comments. Since this is only about the UX case when the CSRF token is invalid, we decided to wait to see if any contrib is meaningfully impacted
So, tried looking at an added method, but that doesn't work since the FAPI array element doesn't have any reference to the corresponding class (and using a class is not required).
Since we do check is_callable() earlier in the FormBuilder, I think type hinting here is reasonable.
Comment #53
dawehnerThank you!
Comment #54
alexpottAdding credit.
Comment #55
alexpottCommitted dcd5951 and pushed to 8.0.x. Thanks!
We need that followup for extensibility by contrib.