user_pass() sets #default_value = $_GET[name] without checking the type.

Created on 15 November 2024, 3 months ago

This issue was discussed by the Drupal Security Team, and their decision was that this can be solved in a public issue.

Problem/Motivation

Original report by @donquixote:

The following is obviously related to SA-CORE-2018-002.
I think it is not currently an open vulnerability, but a weakness or flaw nonetheless.
Perhaps it could play a role in future vulnerabilities.

The function user_pass() is a form builder callback.
It sets a $form['name']['#default_value'] = $_GET['name'] ?? '';, without checking whether it is a string.

function user_pass() {
  global $user;

  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Username or e-mail address'),
    '#size' => 60,
    '#maxlength' => max(USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH),
    '#required' => TRUE,
    '#default_value' => isset($_GET['name']) ? $_GET['name'] : '',
  );

Looks wrong.

For me this causes warnings like this from attackers who are looking to exploit SA-CORE-2018-002 or SA-CORE-2018-004 (but fail, afaik, because the site is up to date):

> Warning: trim() expects parameter 1 to be string, array given in user_pass_validate() (line 69 of ***/modules/user/user.pages.inc).

Request URI is something like this:
/?q=user/password&name[%23post_render][]=passthru&name[%23type]=markup&name[%23markup]=uname+-a

Of course the %23type et al will be filtered out. But it is still putting an array where there should be a string.

Typically I get 2 requests in sequence, and I think it is really shooting at sites that were not updated yet. .

  1. /?q=user/password&name[%23post_render][]=passthru&name[%23type]=markup&name[%23markup]=uname+-a
  2. /?q=file/ajax/name/%23value/form-37ku4BNL8vpFA4nfOjSnbVztvIDEGY9vSpxkLyrEz9E

So, not really new.

But the user_pass() still should not blindly trust what it finds in $_GET.

Steps to reproduce

Proposed resolution

@alexpott commented:

Why wouldn't we do this as a public hardening? Also this approach feels piecemeal - we need to do this for all input not just one thing. I feel that we should have a strong typed system for all form values and user input and if #default_value or #value is not supported by the form element type we should error.

Also I think we should look at measures like https://www.drupal.org/project/drupal/issues/2966327 β†’ first which offer a more generic type of protection as to what user input can do when handled by the render and (eventually) form APIs.

This is present in Drupal 8 too in \Drupal\user\Form\UserPasswordForm::buildForm().

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

7.0 ⚰️

Component

user.module

Created by

πŸ‡ΊπŸ‡ΈUnited States akalata

Live updates comments and jobs are added and updated live.
  • Security improvements

    It makes Drupal less vulnerable to abuse or misuse. Note, this is the preferred tag, though the Security tag has a large body of issues tagged to it. Do NOT publicly disclose security vulnerabilities; contact the security team instead. Anyone (whether security team or not) can apply this tag to security improvements that do not directly present a vulnerability e.g. hardening an API to add filtering to reduce a common mistake in contributed modules.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @akalata
  • Merge request !10204Add @donquixote's patch β†’ (Open) created by akalata
  • Pipeline finished with Success
    3 months ago
    Total: 245s
    #339986
  • πŸ‡¬πŸ‡§United Kingdom alexpott πŸ‡ͺπŸ‡ΊπŸŒ
  • πŸ‡¬πŸ‡§United Kingdom catch
  • πŸ‡ΈπŸ‡°Slovakia poker10

    Should this target 11.x, instead of 7.x, because of this?

    This is present in Drupal 8 too in \Drupal\user\Form\UserPasswordForm::buildForm().

  • First commit to issue fork.
  • Pipeline finished with Failed
    3 months ago
    Total: 221s
    #342449
  • πŸ‡¬πŸ‡§United Kingdom catch

    Re #6 I think this is no longer an issue in 10.x/11.x due to:

    https://github.com/symfony/symfony/blob/6.4/src/Symfony/Component/HttpFo... e.g. a BadRequestException is already thrown by Symfony if the value retrieved in ::get() isn't a string.

  • Pipeline finished with Success
    3 months ago
    #342473
  • πŸ‡ΈπŸ‡°Slovakia poker10

    Thanks for adding the test and working on this. Also thanks for the confirmation about 10.x/11.x.

    The warning Warning: trim() expects parameter 1 to be string, array given in user_pass_validate() (line 69 of ***/modules/user/user.pages.inc). from the IS was fixed by #2975433: Notices/warnings from attempted exploitation of SA-CORE-2018-002 and SA-CORE-2018-004 β†’ .

    Therefore I think it should be sufficient to protect the #default_value and allow only scalar types. I am not sure if there is any notable benefit of the additional str_replace:

    str_replace(array("\r", "\n"), '', (string) $_GET['name']);
    

    What if there will be a newline in the string? Will that cause any problems? If not, I think we can simplify the patch to just check for is_scalar (without the str_replace call). Any thoughts? Otherwise the hardening looks good to me.

  • πŸ‡¬πŸ‡§United Kingdom mcdruid πŸ‡¬πŸ‡§πŸ‡ͺπŸ‡Ί

    Yeah I'm not sure we need the replacement of the newline characters?

    Along the lines of @alexpott's comment (which is now outdated for D10/11, IIUC) are there any other form fields we might want to add this to as well?

    I'm thinking particularly of those that are typically exposed to anon users like the (rest of the) login form, perhaps search etc..?

  • πŸ‡ΈπŸ‡°Slovakia poker10

    I think that the answer to the question may be what @donquixote wrote in the private issue:

    The '#default_value' is usually coming from a somewhat trusted source, where we know the type.
    So I think the (unwritten) rule is that whoever sets #default_value is responsible for the value.

    This case is special because the #default_value is set with a value that could be anything (anything that can come from $_GET, which means at least any string or array). This is why I think a "piecemeal" or one-instance fix is the correct thing to do here.

    Currently the #value_callback, or automatic value callback like form_type_textfield_value() in D7, is responsible for sanitizing form data from $form_state['input'] before it goes into #value.
    The #default_value is not sanitized automatically in this way, it is taken as-is and written into #value if there is no form input.

    So I think the difference between this field and other inputs (that can be only manually filled) is that this one can be prefilled from URL and the value is written to the "mostly trusted" #default_value. But yes, looking at the fix from #2975433: Notices/warnings from attempted exploitation of SA-CORE-2018-002 and SA-CORE-2018-004 β†’ , I am thinking, whether we even need this fix? Because the main reason was to sanitize $form_state['values']['name'] and that is already done by the is_scalar() right in the beginning of user_pass_validate(). On the other hand yes, you can alter the form and access the value sooner, than the default validate callback runs (but that is also questionable, if core does the sanitization, if we should sanitize it even in case contrib/custom code unsets or delays our validation).

  • πŸ‡¬πŸ‡§United Kingdom mcdruid πŸ‡¬πŸ‡§πŸ‡ͺπŸ‡Ί

    Yeah, I think you're right that we've already addressed the problem described in the IS:

    Warning: trim() expects parameter 1 to be string, array given in user_pass_validate() (line 69 of ***/modules/user/user.pages.inc).

    It doesn't look like that could happen any more; core's checking is_scalar pretty early on in the form processing.

    This was not the case when the (private) issue was originally filed.

    I think we can close this as outdated, but thanks everyone that's worked on it.

Production build 0.71.5 2024