Closed (fixed)
Project:
Drupal core
Version:
7.x-dev
Component:
database system
Priority:
Critical
Category:
Bug report
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
19 Sep 2016 at 08:00 UTC
Updated:
9 Dec 2021 at 01:09 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
hgoto commentedHere 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 :)
Comment #3
hgoto commentedI failed to attach the capture...
Comment #5
hgoto commentedThe 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.
Comment #6
stefan.r commentedLooks great, thanks!
Just wondering, why is this marked as 7.60 target?
Comment #7
hgoto commented@stefan.r thank you for your review!
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...
Comment #8
stefan.r commentedAssigning to Fabianx for review
Comment #10
David_Rothstein commentedI 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.
I tested this out and it doesn't seem to be doing what it's intending to do:
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 = FALSEand$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).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....
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.
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.
Comment #11
stefan.r commentedNice 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?
Comment #12
David_Rothstein commentedYes, 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.
Comment #13
hgoto commented@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.)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()):D8 (
Drupal\Core\Database\Query\Condition::__toString()):D7's
__toString(), if$this->changedis 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->changedis 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.
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.
InvalidQueryConditionOperatorExceptionand made thecompile()method throw this exception instead of doing the previous logic.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)
Comment #15
hgoto commentedI've found one thing. Please wait a little before reviewing the newest patch.
I said in the previous comment that:
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 callingexecute()or something.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.Comment #16
hgoto commentedSorry, double posts.
Or, currently we are trying to validate the condition operator when
DatabaseCondition::compile()is called. It's called whenSelectQuery::execute()orSelectQuery::__toString()is called to run an actual query to database. But what if we validate the conditions when the parameter passed intoDatabaseCondition::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...
Comment #17
stefan.r commentedComment #18
hgoto commented@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.
I think, our target
DatabaseConditionclass only has 2 public methods to add/modify the WHERE clause it generates.condition()andwhere(). 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 considercondition(). So we can move the validation logic fromcompile()tocondition().condition()is not in__toString()(condition()is invoked earlier thancompile()) 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.)
Comment #19
hgoto commentedComment #22
hgoto commentedIt works as expected. I'd like someone to review this approach.
Comment #23
hgoto commentedThis issue is not same but similar.
#829464: orderby() should verify direction and escape fields
Comment #24
joseph.olstadLooks 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 !
Comment #25
stefan.r commentedComment #26
David_Rothstein commentedSince 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.
Comment #27
David_Rothstein commentedI 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.
Comment #29
joseph.olstadI 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.
Comment #30
joseph.olstadComment #31
joseph.olstadBumping to 7.61. This didn't make it into 7.60.
Comment #32
joseph.olstadComment #33
joseph.olstadComment #34
mustanggb commentedComment #35
mustanggb commentedComment #36
joseph.olstadComment #37
mcdruid commentedReroll of patches from #27
This has already been pretty thoroughly reviewed (and is a backport).
Comment #39
mcdruid commentedI 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:
...fails with an exception like:
..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:
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.Comment #40
mcdruid commented#2492967-58: SQL layer: $match_operator is vulnerable to injection attack is where SQLite operators are mentioned.
IIUC the proposal was to add
--alongsideUNIONin 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.
Comment #54
mcdruid commentedAdding credit from the original issue.
Comment #56
mcdruid commentedThank you!
Comment #57
joseph.olstad@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!