Only allow string values to be passed if #maxlength is used in FormValidator

Created on 24 January 2022, almost 3 years ago
Updated 19 August 2024, 4 months ago

Problem/Motivation

Frequently I find urls like: example.com/user/password?name%5B%23post_render%5D%5B0%5D=array_map&name%5B%23suffix%5D=eval%28%24_POST%5Bc%5D%29%3B%2F%2F&name%5B%23type%5D=markup&name%5B%23markup%5D=assert

result in the logging: Warning:

mb_strlen() expects parameter 1 to be string, array given in Drupal\Core\Form\FormValidator->performRequiredValidation() (line 333 of /xxx/core/lib/Drupal/Core/Form/FormValidator.php)

I presume the code should give an error message to the user and prevent a warning in the logging.
I Drupal 7 these is warnings exists as well, but are not so important.

Steps to reproduce

From #3:

Here's an example request to reproduce:

POST https://example.com/user/password?name[#post_render][0]=array_map&name[#suffix]=eval($_POST[c]);//&name[#type]=markup&name[#markup]=assert
Content-Type: application/x-www-form-urlencoded

form_build_id=form-dSIbkM-5ZfbqbUs10r9v-nwdAcFbh6h_UZy3jaaFo-0&form_token=b9OVZTBZfjNlJQZC4RsHDwVyWPoNWSerhnS4iz714JM&form_id=user_pass&op=Reset

We have been getting this error on most Drupal sites because of hacking attempts. Not a critical issue, but it would be a big improvement nonetheless if this would stop polluting our logs.

Proposed resolution

In form validation, ensure only string values are allowed if #maxlength is set. Otherwise add a form error.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Form 

Last updated 2 days ago

Created by

🇳🇱Netherlands promes

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇺🇸United States smustgrave

    This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.

    So this almost seems like we are patching the symptom vs the problem. Should we fix why ->get('name'); is returning an array?

    Either way this will require a test case to show the issue.

    Thanks

  • 🇸🇮Slovenia joco_sp

    We got this error on a custom form where we had the datetime field and we had to use it as an array. Which is then also array in the $elements['#value']. For us, the problem occurred after an update to Drupal core 9.5.5 and PHP8. Before that, we didn't get WSOD, but just a warning.

    In my opinion it's good to add a check if the $elements['#value'] is really a string, because if it's not, you are getting a WSOD in PHP8.

    I am attaching a patch that checks if $elements['#value'] is a string. It should work on core 9.5.x

  • 🇩🇪Germany Yusuf.Fidan

    Hat the same error after Upgrade on 10.2.
    Patch isstring_mb_strlen-3260087-13.patch works for me.

  • 🇮🇹Italy apaderno Brescia, 🇮🇹
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & PostgreSQL 12.1
    last update 11 months ago
    25,837 pass, 1,803 fail
  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & pgsql-13.5
    last update 11 months ago
    Build Successful
  • Status changed to Needs review 7 months ago
  • 🇫🇮Finland sthomen

    I just received a spate of these attempts to some of our sites this weekend and I find it a little alarming that drupal assumes the type of the input based on the rendered element that the input happens to correspond to.

    Looking at performRequiredValidation(), the maxlength check seems to be done a little too early, perhaps something like the attached patch instead? With it, I get a 400 instead of a crash and error 500 when sending an "array" instead of the expected in the query string to /user/password.

    Given that this is a pretty critical piece fo safety I would not trust myself to get this right, more eyeballs please :-)

  • Status changed to Needs work 7 months ago
  • 🇫🇮Finland sthomen

    Lets try again to fix my blunder, setting back to needs work.

  • First commit to issue fork.
  • 🇮🇳India immaculatexavier

    immaculatexavier changed the visibility of the branch 3260087-mbstrlen to hidden.

  • 🇳🇱Netherlands ricardopeters

    I've got pointed to this issue from the Drupal Slack, where a likewise URL is crafted/used, but receiving a different error. I think it's useful to at least have a reference to it here.
    Issue 3340577 🐛 TypeError: Argument 1 passed to Egulias\EmailValidator\EmailValidator::isValid() must be of the type string, null given Needs work

  • 🇩🇪Germany Anybody Porta Westfalica

    Just had several of the same entries in log. URL called:
    /user/register?_wrapper_format=drupal_ajax&ajax_form=1&element_parents=account%2Fmail%2F%23value

    MR !2827 seems correct to me and I can't see any feedback saying it doesn't work as expected. So what's left here is a test to pass an array (and NULL?) to the validation and ensure it reacts as expected?

  • 🇫🇷France goz

    I have the same issue in my logs with query like :
    curl http://localhost/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax" -X POST -d "form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail%5B%23markup%5D=TRY_TO_INJECT_SOMETHING"

    I wonder if it make any sense that a field with #maxlength have another #value than a string.

    My first attempt would be the same a #13, except we should not receive an array here. May be we should :

    1. At minimum set form error if @maxlength is setted, but #value is not string. In this case, we cannot prevent some other code will break because we don't expect value is other thing than string
    2. I think the best is to raise a 500 exception since it should not happen
  • 🇩🇪Germany Anybody Porta Westfalica

    Yes, this keeps popping up in several projects, even more frequently now. We should fix this... on it!

  • First commit to issue fork.
  • 🇩🇪Germany Anybody Porta Westfalica

    Anybody changed the visibility of the branch 11.x to hidden.

  • 🇩🇪Germany Grevil

    Grevil changed the visibility of the branch 11.x to hidden.

  • Pipeline finished with Canceled
    5 months ago
    Total: 398s
    #244312
  • Status changed to Needs review 5 months ago
  • 🇩🇪Germany Grevil

    I feel like the approach by @GoZ is pretty good! But instead of throwing a 500er error, we should throw a form state error and return instead. Committed the changes inside MR !9073.

  • Pipeline finished with Success
    5 months ago
    Total: 436s
    #244324
  • 🇩🇪Germany Grevil

    Some further adjustments and added a test.

  • 🇩🇪Germany Grevil

    Grevil changed the visibility of the branch 3260087-warning-mbstrlen-expects to hidden.

  • Status changed to Needs work 5 months ago
  • 🇩🇪Germany Anybody Porta Westfalica

    Great @Grevil, I like that approach!

    Just left one comment for comments, then it's RTBC from my side, if the test also goes green!

  • 🇩🇪Germany Anybody Porta Westfalica

    PS: A native (EN) speaker or core maintainer should please finally review the wording in the error message.

  • Pipeline finished with Success
    5 months ago
    Total: 509s
    #244365
  • Status changed to RTBC 5 months ago
  • 🇩🇪Germany Grevil

    Tests are green, setting this RTBC'd.

  • Status changed to Needs work 5 months ago
  • 🇬🇧United Kingdom catch

    I think @smokris comment in the MR makes sense, moving to needs work for that.

  • 🇫🇷France goz

    I suggested (and push for it) to use 500 error instead of form error for the following reasons :

    - in my tests, if I set a form error, other validators still process, and also fail with 500 (with generic answer, hard to diagnostic)
    - this issue can occur mainly for two reasons : an attack, or bad code. In second case, developer should deal with the 500 error in its tests.
    - on cases reported by this issue, this issue appear because of bad intentions form an attacker. We should stop it as soon as possible (so 500)

  • 🇫🇷France goz

    Thinking about it, if we want to use form error and avoid issues reported by my previous post, maybe we could clear values after setting form error, so malformed values (array) cannot report error or be a risk anymore.

  • 🇳🇿New Zealand quietone

    The issue summary is incomplete, so I added the template. At least, the proposed resolution should be completed to help reviewers and committers.

    This also needs an title update. The title should be a description of what is being fixed or improved. The title is used as the git commit message so it should be meaningful an concise. See List of issue fields .

  • Pipeline finished with Canceled
    5 months ago
    Total: 70s
    #251690
  • 🇩🇪Germany Anybody Porta Westfalica
  • Status changed to Needs review 5 months ago
  • 🇩🇪Germany Anybody Porta Westfalica

    @quietone okay like this?

  • 🇩🇪Germany Anybody Porta Westfalica
  • Pipeline finished with Success
    5 months ago
    Total: 622s
    #251691
  • Status changed to Needs work 4 months ago
  • 🇺🇸United States smustgrave

    Appears to be 1 open thread that appears valid.

  • Status changed to Needs review 4 months ago
  • 🇩🇪Germany Grevil

    Resolved everything.

  • Status changed to Needs work 4 months ago
  • 🇬🇧United Kingdom alexpott 🇪🇺🌍

    Hmmm... looking at this code - it is called a lot - for each element in a form. I think we should make a very conservative change here. Something like:

        // Verify that the value is not longer than #maxlength.
        if (isset($elements['#maxlength'])) {
          $name = empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'];
          if (!is_string($elements['#value'])) {
            $form_state->setError($elements, $this->t('The submitted value type %type in the %name element is not allowed.', ['%type' => gettype($elements['#value']), '%name' => $name]));
          }
          elseif (mb_strlen($elements['#value']) > $elements['#maxlength']) {
            $form_state->setError($elements, $this->t('@name cannot be longer than %max characters but is currently %length characters long.', ['@name' => $name, '%max' => $elements['#maxlength'], '%length' => mb_strlen($elements['#value'])]));
          }
        }
    

    This keeps two behaviours from the existing code - it only does the empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']; evaluation if the #maxlength is set (yes it did this already) and it also continues to evaluate the code below if max length is set and an error is set. I.e. no early return.

  • 🇩🇪Germany Grevil

    @alexpott Sure thing, one extra layer of safety won't hurt.

  • Status changed to Needs review 4 months ago
  • 🇩🇪Germany Grevil

    Adjusted accordingly, please review again.

  • Pipeline finished with Failed
    4 months ago
    Total: 147s
    #258052
  • Status changed to Needs work 4 months ago
  • 🇩🇪Germany Grevil

    Now we have an implementation closer to how it was before, but $name might not be declared in line 345.

    So now we have to either revert fae394a7 and live with the unlikely case, that $elements['#parents'][0] or $elements['#title'] do not exist OR we find a valid fallback for $name for the message arguments in line 345.

Production build 0.71.5 2024