Comments

hgoto created an issue. See original summary.

hgoto’s picture

Status: Active » Needs review
StatusFileSize
new5.4 KB
new3.72 KB
new1.52 KB

Here is a backport patch. I changed the test code a little bit from patch #87 on #2492967: SQL layer: $match_operator is vulnerable to injection attack to make it cleaner. But it's almost same as that.

This test-only patch will raise CI error because PDOException occurs for broken SQL. So I attached a capture for my local test report.

I'd like this to be reviewed.

The following is a part of my original comment when I posted the first D7 patch. This must be helpful for review :)

I recognize that there are 5 classes/functions changed in the patch #73 for D8.

  • Drupal\Core\Database\Query\Condition
  • Drupal\system\Tests\Database\DatabaseWebTestBase
  • Drupal\system\Tests\Database\QueryTest
  • Drupal\Tests\Core\Database\ConditionTest
  • database_test_schema()

If I understand it correctly, We can backport four among them. The exception is ConditionTest. It's difficult to backport it because ConditionTest uses test mocks with PHPUnit_Framework_TestCase::prophesize() (Prophecy\Prophecy\ObjectProphecy) and D7 Simpletest doesn't have such functionality, as far as I know. (I may be wrong... If I'm wrong, please tell me.)

A mapping of matching classes/functions between D8 and D7 is as following.

D8 -> D7:

  • Drupal\Core\Database\Query\Condition -> DatabaseCondition
  • Drupal\system\Tests\Database\DatabaseWebTestBase -> DatabaseTestCase
  • Drupal\system\Tests\Database\QueryTest -> DatabaseQueryTestCase
  • Drupal\Tests\Core\Database\ConditionTest
  • database_test_schema() -> database_test_schema()

Under this understanding, I created a patch for D7.

hgoto’s picture

StatusFileSize
new369.44 KB

I failed to attach the capture...

Status: Needs review » Needs work

The last submitted patch, 2: 2802159-2-test_only_should_fail.patch, failed testing.

hgoto’s picture

Status: Needs work » Needs review

The status was changed to "Needs work" automatically. But that CI error was generated by the test-only patch as expected. I'd like to change the status to NR again.

stefan.r’s picture

Status: Needs review » Reviewed & tested by the community

Looks great, thanks!

Just wondering, why is this marked as 7.60 target?

hgoto’s picture

@stefan.r thank you for your review!

Just wondering, why is this marked as 7.60 target?

I'm sorry, I added the same tags as the D8 issue #2492967: SQL layer: $match_operator is vulnerable to injection attack has without thinking well... I guessed that if a site makes use of this vulnerability in a good way(?), such functions will be broken. That's why...

stefan.r’s picture

Assigned: Unassigned » fabianx

Assigning to Fabianx for review

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 2: 2802159-2-test_only_should_fail.patch, failed testing.

David_Rothstein’s picture

I guess this patch is relatively safe to do - seems like it could only cause problems if people are using the $operator parameter very improperly. So I don't have a strong opinion about doing it in 7.60 vs. a regular bugfix release.

  1. +              $this->changed = TRUE;
    +              $this->arguments = array();
    +              // Provide a string which will result into an empty query result.
    +              $this->stringVersion = '( AND 1 = 0 )';
    +
    +              // Conceptually throwing an exception caused by user input is bad
    +              // as you result into a WSOD, which depending on your webserver
    +              // configuration can result into the assumption that your site is
    +              // broken.
    +              // On top of that the database API relies on __toString() which
    +              // does not allow to throw exceptions.
    +              trigger_error('Invalid characters in query operator: ' . $condition['operator'], E_USER_ERROR);
    +              return;
    

    I tested this out and it doesn't seem to be doing what it's intending to do:

    • First, I wound up with a WSOD + error page anyway (with message Recoverable fatal error: Method DatabaseCondition::__toString() must return a string value in SelectQuery->__toString() (line 1553 of /.../drupal/includes/database/select.inc)). I tried doing $this->changed = FALSE and $this->stringVersion = '( 1 = 0 )' instead and then it worked as expected (although the latter probably depends on the exact nature of the query that's running in order to actually work).
    • I think the code comment about not being able to throw exceptions here is wrong. I tried throwing an exception here instead and it seemed to work fine for me.
    • I don't completely agree with the goal of avoiding a WSOD. Well I do in theory, but it's kind of silly because we're only avoiding it in a very narrow case... If someone passes in user input that doesn't contain one of the disallowed characters but still isn't a valid operator (for example, "ELEPHANT") you'll get a WSOD + error page then anyway since the database query will fail.

    So overall this would be a lot simpler if we just threw an exception here - any good reason not to do that? It's probably worth a followup issue to clean this up for Drupal 8 also.

    By the way, in case anyone is wondering (I was) passing the $condition['operator'] to trigger_error() doesn't replace the SQL injection with XSS since Drupal filters the message in https://api.drupal.org/api/drupal/includes%21errors.inc/function/_drupal....

  2. +    // Convert errors to exceptions for testing purposes below.
    +    set_error_handler(function ($severity, $message, $filename, $lineno) {
    +      throw new ErrorException($message, 0, $severity, $filename, $lineno);
    +    });
    

    Don't think that would work on PHP 5.2. This may not be necessary anyway if we change it to just throw an exception as suggested above.

  3. +    // Attempt SQLi via union query with no unsafe characters.
    +    module_enable(array('user'));
    +    // $this->resetAll();
    

    Not sure what's going on here, but the User module is required so I don't think it needs to be enabled. And the commented out line can be removed too.

  4. I just did a retest and it looks like we aren't getting a clean failure with the test-only patch (only CI errors) ... perhaps that is a consequence of some of the above?
stefan.r’s picture

Assigned: fabianx » Unassigned

Nice spots! I think in D8 we have a workaround for the __toString issue (but I may be wrong), and throwing an exception during __toString() would still give the same issue in D7, but it seems you manually tested that it didn't?

David_Rothstein’s picture

Yes, it seemed to work fine when I tried it quickly. I think maybe what happens is that the exception is getting thrown earlier (before the query object is converted to a string). Not sure if that's a different behavior than Drupal 8.

hgoto’s picture

Status: Needs work » Needs review
StatusFileSize
new5.44 KB
new3.48 KB
new5.2 KB

@David_Rothstein @stefan.r thank you very much for your review and assist. I investigated this a little deeper and found that David_Rothstein is right.

1.

First, as David_Rothstein told, we seem to be able to throw an exception here. Because this compile() method doesn't seem to be called inside of any __toString(). (I'm not 100% sure, but at least, I also manually tested one query pattern like following one and confirmed I can.)

function mymodule_init() {

  $query = db_select('node', 'n')
    ->fields('n')
    // a valid pattern
    // ->condition('nid', 1)
    // an invalid pattern which is caught
    ->condition('nid', 1, ';')
    // an invalid pattern which is not caught
    // ->condition('nid', 1, 'ELEPHANT')
  ;

  $result = $query->execute()->fetchAll();

}

2.

I investigated the reason why we get the __toString() error in D7 but we don't get in D8. It seems to be caused by the following difference.

D7 (DatabaseCondition::__toString()):

  /**
   * Implements PHP magic __toString method to convert the conditions to string.
   *
   * @return string
   *   A string version of the conditions.
   */
  public function __toString() {
    // If the caller forgot to call compile() first, refuse to run.
    if ($this->changed) {
      return NULL;
    }
    return $this->stringVersion;
  }

D8 (Drupal\Core\Database\Query\Condition::__toString()):

  /**
   * Implements PHP magic __toString method to convert the conditions to string.
   *
   * @return string
   *   A string version of the conditions.
   */
  public function __toString() {
    // If the caller forgot to call compile() first, refuse to run.
    if ($this->changed) {
      return '';
    }
    return $this->stringVersion;
  }

D7's __toString(), if $this->changed is set to TRUE, returns NULL and that leads to a PHP fatal error. On the other hand, D8's __toString() always returns a string (an empty string) even if $this->changed is TRUE. I'm not sure if this is an intended difference. If we want to stop PHP here before making actual query to database, returning NULL may be rational. I'm not sure which is better one here...

3.

I agree with David_Rothstein on the following point.

I don't completely agree with the goal of avoiding a WSOD. Well I do in theory, but it's kind of silly because we're only avoiding it in a very narrow case... If someone passes in user input that doesn't contain one of the disallowed characters but still isn't a valid operator (for example, "ELEPHANT") you'll get a WSOD + error page then anyway since the database query will fail.

4.

Anyway, throwing an exception here seems to be possible and I'd like to propose a new patch following the feedbacks. I updated the following points.

  • Created a new database exception InvalidQueryConditionOperatorException and made the compile() method throw this exception instead of doing the previous logic.
  • Simplified the test and removed module_enable(array('user')); and unnecessary comments in the test.

I'm sorry to have proposed the previous patch without understanding it correctly... I hope this one works :)

(I also tested manually if we can throw an exception here or not in D8 and I could.)

(Edited)

Status: Needs review » Needs work

The last submitted patch, 13: 2802159-13-test_only_should_fail.patch, failed testing.

hgoto’s picture

I've found one thing. Please wait a little before reviewing the newest patch.

I said in the previous comment that:

Because this compile() method doesn't seem to be called inside of any __toString(). (I'm not 100% sure, but at least, I also manually tested one query pattern like following one and confirmed I can.)

but I found that DatabaseCondition::compile() may be called inside of __toString() in the following order if we use (string) $query (stringify an instance of any query object) before calling execute() or something.

SelectQuery::__toString() -> SelectQuery::compile(). -> DatabaseCondition::compile()

https://api.drupal.org/api/drupal/includes%21database%21select.inc/funct...

Probably we need to investigate more. If this compile() can be called inside __toString(), we shouldn't throw an exception there.

hgoto’s picture

Sorry, double posts.

Or, currently we are trying to validate the condition operator when DatabaseCondition::compile() is called. It's called when SelectQuery::execute() or SelectQuery::__toString() is called to run an actual query to database. But what if we validate the conditions when the parameter passed into DatabaseCondition::condition(). It happens in so earlier timing that we can just throw an exception, maybe. (This point may have been already discussed in the D8 thread but I don't understand.)

https://api.drupal.org/api/drupal/includes%21database%21query.inc/functi...

stefan.r’s picture

hgoto’s picture

StatusFileSize
new4.91 KB
new3.48 KB
new1.77 KB

@stefan.r thank you for the link. The technique can be used here, too.

Before that, please let me try going ahead to the direction I told in the previous comment.

But what if we validate the conditions when the parameter passed into DatabaseCondition::condition(). It happens in so earlier timing that we can just throw an exception, maybe.

I think, our target DatabaseCondition class only has 2 public methods to add/modify the WHERE clause it generates. condition() and where(). We must use either of them to specify a query condition.

https://api.drupal.org/api/drupal/includes%21database%21query.inc/class/...

And, where() method doesn't take any operator and we only need to consider condition(). So we can move the validation logic from compile() to condition(). condition() is not in __toString() ( condition() is invoked earlier than compile()) and we can throw an exception without worrying __toString() problem, I think.

I'd like to try a patch with this idea. (And I'd like this reviewed.)

hgoto’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 18: 2802159-18-test_only_should_fail.patch, failed testing.

The last submitted patch, 18: 2802159-18-test_only_should_fail.patch, failed testing.

hgoto’s picture

Status: Needs work » Needs review

It works as expected. I'd like someone to review this approach.

hgoto’s picture

joseph.olstad’s picture

Status: Needs review » Reviewed & tested by the community

Looks good, without the patch, the new sql injection test fails.

with the patch, the new sql injection test passes.

Is there a priority higher than Critical? lol !

stefan.r’s picture

Assigned: Unassigned » David_Rothstein
David_Rothstein’s picture

Since we're probably pretty close to a 7.60 release and there is a bit of risk with this patch, I think we should hold it off for Drupal 7.60 rather than pushing this in now a few days after it was marked RTBC. Will try to review it soon though.

David_Rothstein’s picture

Assigned: David_Rothstein » Unassigned
Status: Reviewed & tested by the community » Needs review
StatusFileSize
new3.48 KB
new10.7 KB
new6.17 KB

I think there is a problem with the newer approach of checking the operator inside the condition() method. It won't protect against SQL injection in cases where the condition was modified later on (after it was created). An example of code that modifies the conditions later on can be found in TaxonomyTermController::buildQuery(). If that were susceptible to SQL injection (it isn't, but if it were) the patch in #18 wouldn't protect against it.

So I think it is better to go back to the approach in #13.

And nice find (in #15) that this code can be called from within a __toString() method after all. However, it looks like that is not a common scenario (most database queries won't be converted to a string before being compiled) so it would be a shame to lose the nice InvalidQueryConditionOperatorException entirely. I think maybe we can throw the InvalidQueryConditionOperatorException and use it most of the time, but catch it within the __toString() methods and explicitly trigger a fatal error only then?

I have attached a patch that does this. For triggering the fatal error, I borrowed some ideas from #2562487: Get better error reporting from __toString but decided it would be better (both for backwards-compatibility and for making the code a bit more self-documenting) to do it a little differently in Drupal 7. So I introduced a new function drupal_trigger_fatal_error() that can be used to force the fatal error.

Hopefully we can get some reviews of this and get the issue fixed soon.

Note: The interdiff is against #13.

The last submitted patch, 27: 2802159-27-TESTS-ONLY.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

joseph.olstad’s picture

I like the try catch blocks.
This latest patch looks really good as far as the interdiff is concerned.

I applied the patch, it applied cleanly.
cleared caches,
Ran a few manual tests, everything looks good.

joseph.olstad’s picture

Status: Needs review » Reviewed & tested by the community
joseph.olstad’s picture

Issue tags: -Drupal 7.60 target +Drupal 7.61 target

Bumping to 7.61. This didn't make it into 7.60.

joseph.olstad’s picture

joseph.olstad’s picture

mustanggb’s picture

Issue tags: -Drupal 7.64 target +Drupal 7.69 target
mustanggb’s picture

Issue tags: -Drupal 7.69 target +Drupal 7.70 target
joseph.olstad’s picture

mcdruid’s picture

Issue tags: -Drupal 7.74 target
StatusFileSize
new3.54 KB
new10.67 KB

Reroll of patches from #27

This has already been pretty thoroughly reviewed (and is a backport).

The last submitted patch, 37: 2802159-27_test_only_reroll.patch, failed testing. View results

mcdruid’s picture

I think this is looking pretty good (still trying to get a clean "green board" for the test runs though).

One thing that put me off a little bit is that the test only patch mostly doesn't fail in quite the way we might expect.

This was pretty much covered in #2492967-35: SQL layer: $match_operator is vulnerable to injection attack where @larowlan mentioned that the DB API won't allow a "little bobby tables"-style SQLi where a whole additional query is injected. So the test which tries to use this as an operator:

$injection = "IS NOT NULL); INSERT INTO {test} (name) VALUES ('test12345678'); -- ";

...fails with an exception like:

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You
    have an error in your SQL syntax; check the manual that corresponds to your
    MySQL server version for the right syntax to use near 'INSERT INTO
    `simpletest673306TEST` (NAME) VALUES ('TEST12345678'); --

..even without the actual changes in the patch here. Of course, that's not a bad thing in itself.

Looking more closely at the test, this doesn't matter too much though as we're positively checking for the right exception being thrown once it's been added in the patch:

    catch (InvalidQueryConditionOperatorException $e) {
      $this->pass('SQL injection attempt via condition arguments should result in a database exception.');
    }

So the test passes mean the new check is catching the injection attempts and throwing the new exception.

In other words, I think this LGTM (but I took a little while checking through exactly what's going on in the tests).

There was also one comment in the previous issues though about - perhaps being a valid character in operators in SQLite. I want to have another look at that before committing this.

mcdruid’s picture

#2492967-58: SQL layer: $match_operator is vulnerable to injection attack is where SQLite operators are mentioned.

IIUC the proposal was to add -- alongside UNION in the blocklist, and remove the single - from the blocked list of characters.

However, that doesn't seem to have happened and D9 still has a pretty similar implementation to the patch we're considering here:

https://git.drupalcode.org/project/drupal/-/blob/9.3.0-beta3/core/lib/Dr...

It's conceivable that an operator that works (only) in SQLite may be broken by this additional check, but I don't think that's worth holding up this patch. A change to the blocklist(s) would be something to address in D9 and backport.

mcdruid credited Berdir.

mcdruid credited Fabianx.

mcdruid credited bircher.

mcdruid credited catch.

mcdruid credited chx.

mcdruid credited cilefen.

mcdruid credited dawehner.

mcdruid credited greggles.

mcdruid credited larowlan.

mcdruid credited pwolanin.

mcdruid credited webchick.

mcdruid’s picture

Adding credit from the original issue.

  • mcdruid committed 093e02f on 7.x
    Issue #2802159 by hgoto, David_Rothstein, mcdruid, bircher, Fabianx,...
mcdruid’s picture

Status: Reviewed & tested by the community » Fixed

Thank you!

joseph.olstad’s picture

@mcdruid, thanks, great job on all the past few months especially for php 8.0 support (and 8.1) and previously the great work on the sqlite and MySQL 8 support!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.