Transferred from https://security.drupal.org/node/134653 since this was determined to be a feature request for FAPI, as opposed to a security vulnerability.
Problem/Motivation
Example 1 reported by hejazee
==================
some form validators should take precedence. for example the validator should be executed first.
and if it fails, other validators should not be executed.
because form validators set messages on the form.
and if the initial validation fails, the user (attacker bot) should not see if other fields are valid since this could expose unwanted information.
Example 2 reported by jpcondon (private issue)
==================
This module has a denial of service vulnerability.
You can see this vulnerability by:
1. Install Drupal.
2. Create content type with file field with two different restrictions on it, such as file size and file type.
3. Upload file with wrong file type and wrong file size and observe that both error messages appear.
While this isn't a big deal when simply validating file type or size, this presents a larger issue when using a module like the ClamAV module where it will attempt to scan the file even if other file validation errors have occurred. If a file is excessively large it will still be scanned which could cause a denial of service on the website.
Proposed resolution
Create a cancel validation variable, getter, and setter in the form state to track whether subsequent form validation functions should be skipped, i.e. if it is preferred that further validation functions should be prevented from executing, e.g.: you might set setValidationCanceled(TRUE) if a CSRF token is not valid, because it doesn't make sense to perform any further validation (and it might be a security risk to do so).
Remaining tasks
Decide how to add this feature in a backward-compatible way.
Commentary from @David_Rothstein:
There probably isn't a way to actually prevent other validation functions from running currently, but there should be some ways to prevent their error messages from displaying... Here's what I wrote elsewhere:
I don't think we necessarily need a change to Drupal core for this. I haven't looked too deeply into it, but couldn't you at least use something like form_get_errors(), form_set_error(), and form_clear_error() to clear out all validation errors on the form (at the end of the validation process) and then re-set the ones from the desired field only? I think something like this technique might work for both Drupal 6 and Drupal 7.
Also for Drupal 7 there's $form_state['triggering_element']['#limit_validation_errors'] which is a cleaner way to tell Drupal to ignore (and not display) certain form validation errors, but perhaps there's no place in the code where you can set that during form validation and have it work on the rest of the validation... not sure.
User interface changes
n/a
API changes
Some new public functions to handle cancellation:
- FormValidator::cancelValidation
- FormStateInterface::setValidationCanceled
- FormStateInterface::isValidationCanceled
See change record for more details.
Data model changes
n/a
| Comment | File | Size | Author |
|---|---|---|---|
| #68 | 2561295-nr-bot.txt | 2.29 KB | needs-review-queue-bot |
Issue fork drupal-2561295
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
pwolanin commentedComment #3
scor commentedComment #4
scor commentedComment #5
David_Rothstein commentedComment #6
tim.plunkettI need to double check that this doesn't have any unwanted side-effects on the validation flow after executeValidateHandlers() is run, but something like this might work.
Comment #7
tim.plunkettRemoved excess use statements I added by mistake.
Comment #13
tim.plunkettIt's not going to apply cleanly because the 8.1.x branch is so far behind 8.0.x.
Comment #28
anas_maw commentedPatch in #7 does not apply and not working as expected
Here is an updated one which is working fine for me
Comment #30
anybodyAs this would still make sense for modules like CAPTCHA (#3202776: [PP-1][2.x] Do not execute other form validations if CAPTCHA is wrong) or other security modules like honeypot, which should definitely run their validation before other requests are made, should I reroll the patch for Drupal 10.1.x as MR?
I guess it might be good to have a core maintainer discussion (and decision) about this first, before investing further time here?
See the linked issues for a real-world benefit and use case.
Comment #31
damienmckennaShould it be something that each form validator could control? e.g.
Comment #32
anybodyOr we use different types of errors or flags for the errors in the validation callback?
I guess in most cases the error in validation should determine if other validations may run or not.
$form_statemight be the right place for that and nice DX? :)Something like:
Note:
$form_state->setCriticalErrorByName();(+ Critical)or
Note:
$form_state->cancelValidation();Just an idea...
That flag might even be set TRUE / FALSE:
Each hook first would check if
$this->getGancelValidation()is true or false.Of course returning TRUE or FALSE from the validation hook is also fine, but less transparent and self-explaining perhaps and if anyone is already returning something from the validation hook already, it may break things... Adding such a method won't break anything.
Comment #33
aleexgreen commentedAnybody, I like your suggestion of
cancelValidation(), I tried implementing it in this patch. Since it is so different from the previous patches, I just wrote it from scratch, so there is no interdiff. (I created this patch on 9.5 because that's how my test environment was setup at the time). The only place there might be a problem (the patch may not apply) is in the test, if that's the case, I'll fix it in short order.Comment #34
aleexgreen commentedPatch applied successfully, but test bot had opinions on spelling and indentation. Here's a new patch generated against 10.1 this time.
Comment #35
anybodyThank you @AlexGreen, that's looking good! :)
What do @tim.plunkett, @David_Rothstein, @DamienMcKenna and the others think about this way? Would be nice to get feedback to be able to proceed here. Who else should be involved?
Comment #36
borisson_Tagging for subsystem maintainers review to answer #35
Comment #37
tim.plunkettThis change (and the non-fail-ing-ness of the patch) mean that we're missing test coverage. Because now nothing is calling
finalizeValidation(), which means nothing is callingsetValidationComplete(), which means that$form_state->isValidationComplete()will start returning FALSEI have no recollection of this issue, but apparently I wrote a patch for this 8 years ago? Cool.
Comment #38
aleexgreen commented@tim.plunkett Is this what you meant?
Comment #39
tim.plunkettOh yeah, the interdiff shows it clearly. With that last fix, we're no longer changing existing behavior 👍🏻
Comment #40
smustgrave commentedReal nitpicky but can the new functions have a typehint return?
Also think it would benefit from a change record for the new functions. Maybe an example of how they can be used.
Comment #41
aleexgreen commentedI'll write a change record for this.
Comment #42
aleexgreen commentedHere is a draft change record, any suggestions/reviews welcome https://www.drupal.org/node/3360541
Comment #43
aleexgreen commented@smustgrave Here is a new patch with the return typehints.
Comment #44
aleexgreen commentedNot exactly sure how to fix this error, it was working in #38 and I didn't change this code. @smustgrave
Comment #45
smustgrave commentedReran #38 and see the same. Most likely another ticket was merged and will have to update this one. Should see if that method is on the class being mocked anymore or if it moved
May have some time tomorrow to help take a look.
Comment #46
tim.plunkettComment #47
tim.plunkett#3351379: Only mock methods defined in interfaces changed the existing usages of stdClass in this test, changing this one to match.
And switch to an MR.
Comment #49
smustgrave commentedUpdated the CR to be introduced in 10.2.
Comment #51
tunicI've rebased 561295-one-form-validation to 11.x so it is mergeable again. I've done locally because for some reason Gitlab was not able to do it, when I clicked the rebase button Gitlab complain about an unknown error.
Comment #52
catchLeft some comments on the MR.
Apart from those, I'm slightly concerned about the 'canceled' naming. Canceled usually means something is called off, but this is short circuiting/finishing early. The word that came to mind is 'terminate' i.e. setValidationTerminated() etc. Or even just 'finished' like 'setValidationFinished()'?
Comment #53
aleexgreen commented@catch In this case, we think "terminated" is the closer concept that this change is intending to signify. We could change it to setValidationTerminated() if that's desired.
Comment #54
smustgrave commentedFor the open threads. Seems at minimum comments were requested to be updated.
Comment #55
smustgrave commentedComment #56
aleexgreen commented@catch I have left a comment on the merge request regarding the question raised in your review.
Comment #57
catchReplied in the MR - my initial review was completely wrong, but the change you suggested makes it a lot more obvious what's happening and should prevent others from getting confused too, so looks great.
Comment #58
tunicThis patch allows validators to cancel validation and I think is a good improvement. However, it would be great to be able to prioritize validations as said in the issue report. So, for example, taking example 2, Clam AV scan would have a low priority so an uploaded file is only scanned if everything else is valid. Or let's think on a form with some kind of authorization field and some other fields: if the authorization field is wrong the other fields are not validated, avoiding information leaking about those fields.
Currently, validators are run from bottom to top while traversing the form array. We could just collect them and run in the given priority. The format of the validation callback would change form just a callback name to an object with the callback and the priority.
Currently, you can only prioritize validators in the same level (or form elem: the root elem or any single elem) but not between them.
With priorities this improvement would be even more useful. Should I create a follow up about this? Discuss it somewhere?
Comment #59
catch@tunic a follow-up for that sounds good.
Comment #60
tunicFollow-up added!
#3396235: Add priority to form validators
Comment #61
aleexgreen commentedReady for re-review @catch
Comment #62
smustgrave commentedSeems that all feedback has been addressed.
Looking at the CR still seems relevant, but could use a 2nd look over.
Comment #63
smustgrave commentedThe failure seems random but I've reran multiple times so wonder if HEAD is broken.
Comment #64
aleexgreen commentedTests are passing, back to RTBC
Comment #65
claudiu.cristea2 questions in MR
Comment #66
quietone commentedI'm triaging RTBC issues. It is always nice to see older issues getting close to commit. Thanks!
I read the IS, skimmed through the comments and the MR, and read the change record. I didn't find any unanswered questions.
I do see that the issue summary has not been updated since 2015. And I think the proposed resolution is out of date. That should be up to date to help reviewers of this issue as well as anyone who winds up here doing any research.
In #62 @smustgrave, thinks the change record is relevant but wants another set of eyes on it. That has not happened. I am excluding myself from that because I am not familiar with the issue. I will add that I think it should begin with a sentence or two to explain the value of this change. Or to answer the question, why, as a developer would I want to use this feature?
Due to the importance of an up to date and accurate proposed resolution section in the Issue Summary I am setting this to Needs work. Also, someone else should check the change record. Fortunately, those steps should be straightforward.
Comment #67
aleexgreen commentedFixed strict typing and updated the proposed resolution to match how we actually resolved the issue.
Comment #68
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #69
tunicIssue reported by the bot has been corrected, so moving to Needs Review again.
Comment #71
smustgrave commentedWould this count as an API change? I feel yes but not familiar with the issue to say. But should be added to issue summary.
Comment #72
tunicUpdated summary with three public functions that are API changes in the current MR.
I also think this would need a change record.
Comment #73
catchThere's already a change record at https://www.drupal.org/node/3360541, it would be useful if people could review it to make sure it's up to date with the current state of the MR though.
Comment #74
tunicOps, I missed that.
I've reviewed the change record and the MR changes and the change record seems up to date to me.
Comment #75
smustgrave commentedBelieve everything is in order. Believe this is good.
Resolved my own thread and applied the suggestion :void to the test.
Comment #76
alexpottNeeds work for @nod_'s comment on the MR.
Comment #77
alexpottAlso - if you cancel validation and there are no errors set yet should the form submit? I don't think it should. I read the code and thought it might not submit - but after re-reading I think it will and I think this is wrong and potentially ends up with way worse security issues.
Also I think we should consider changing the name from cancel validation to halt validation in light of the behaviour change I'm suggesting above.
Comment #78
tunicI agree with @alexpott, if the form cancels validation and there are no errors it seems submit handlers will be run.
This is because FormBuilder::processForm runs submit handlers if form has no errors and is not rebuilding, see line 595:
FormState::hasAnyErrors uses internally a static variable in FormState, $anyErrors. I'm not sure why a static variable is used (maybe because subforms?), but anyway FormState::setAnyErrors is not called anywhere in the MR so that's why I think submit handlers will run.
I think the problem lies in FormBuilder::processForm. It should only run submit handlers if form is not rebuilding AND validation has completed without errors, not just no errors were found.
Either FormBuilder::processForm checks also FormState::isValidationCanceled or FormState provides a way to check validation was completed without errors in a new method. The first approach is easier, but I think is FormState who should be in charge of deciding if the form was completely validated instead of requiring two calls (logic should be inside FormState, not force users of FormState to remember to perform these two checks). However, because the error check uses this static method and an internal static variable, I'm not sure how to do it because I don't know why a static variable is used in the first place.
Also, the name change proposed is a good idea, it's clearer and describes more accurately the proposed behavior.
Comment #79
aleexgreen commented@alexpott, @tunic, thank you for the feedback.
It sounds like are suggesting the following path forward... do you agree? Anything that I missed?
Changeedit: fixedsubstr($callback, 0, 2) == '::'back tostr_starts_with($callback, '::')as per @nod_'s suggestionChangeedit: fixedcancelandcanceledtohaltandhaltedrespectivelyInvestigate where FormState::setAnyErrors is used, and/or git history to see if we can figure out when/why it was made static in the first place.edit:investigatedComment #80
tunic@AleexGreen, from what I've understood and from what I've commented I think you have summed up the current state of the issue perfectly.
Thanks!
Comment #82
aleexgreen commentedFormState::setAnyErrorsandFormState::hasAnyErrorswere created in #2225353: Convert $form_state to an object and provide methods like setError() between comments 19 and 20 by @tim.plunkett in July 2014 for 8.0.x (i.e.: before a Drupal 8.0 release)No explanation given for why they were made static.
According to Sling Academy guide on PHP static properties
Maybe @tim.plunkett intended for “protected static $anyErrors” to be a “flag” that lasts for the duration of the current page request for the current user? (regarding subforms, inline_entity_form existed for D7 at that point, but it’s unclear if that was considered the use-case).
public static function setAnyErrors()is used inpublic function setErrorByName()after recording the element with the error and the error message. It’s also used inpublic function clearErrors()but isn’t defined in an Interface.public static function hasAnyErrors()is defined in FormStateInterface, and is used ~11 times across Drupal core (as of commit bcabbbdb75 to 11.x on 2024-06-03)\Drupal\media_library\Form\AddFormBaseand 3 times in\Drupal\Core\Form\FormBuilderto abort processing\Drupal\Core\Ajax\AjaxFormHelperTrait::ajaxSubmit()to output status messages in an AJAX response\Drupal\media_library\Form\FileUploadForm::validateUploadElementto abort processing and force the user to re-upload a file\Drupal\Core\Form\FormStateDecoratorBase::hasAnyErrors()to be a backend for its own\Drupal\Core\Form\FormStateDecoratorBase::hasAnyErrors()function\Drupal\Tests\Core\Form\FormStateTest::testSetErrorByName()to testsetErrorByName()\Drupal\form_test\Form\FormTestStoragePageCacheForm::validateForm()to know when to cache the formComment #83
aleexgreen commentedCouldn’t decide if it made more sense to throw an exception in
\Drupal\Core\Form\FormSubmitter::executeSubmitHandlersor simply return early.If we throw an exception, then anything calling
FormBuilder::executeSubmitHandlerswill have to catch the exception, and if nothing catches it then the user will get a WSOD (i.e.: possibly from an expired form token because they left the page open in their browser for too long) which seems like a bad user experienceIf we return early, then anything calling
FormBuilder::executeSubmitHandlersdoesn’t really know that something went wrong (i.e.: the node didn't save)P.S. latest commit didn't include tests, was looking for feedback. (Test fails seem to be unrelated)
Comment #84
aleexgreen commentedWhat is the best path to take, throw an exception or return early?
Comment #85
tunicI would say throwing an exception.
I've searched in one of my projects and only found a few references to
executeSubmitHandlersin core, and none on the modules folder (and there are plenty of modules in this project), so it seems it is a very internal function that is not commonly called.Maybe some modules are impacted, but I guess they will not many, and throwing an exception sounds like the right way.
EDIT: Sorry, I added the tag "needs" by mistake, already removed in the next comment, thanks!
Comment #86
borisson_Not sure what this tag was.
Comment #87
smustgrave commentedChecking MR and appears to be throwing errors.