Problem/Motivation
Original report by blasthaus to security.drupal.org - security team decided this is not a vulnerability and can be fixed publicly.
This module has a Denial of Service (DOS) vulnerability.
You can see this vulnerability by:
- Enabling the module or have the module disabled but not yet uninstalled and have set both
- "Count content views" and
- On D7 only, "Use Ajax to increment the counter" module settings.
- As someone with no permission, send a post request to the path
/modules/statistics/statistics.phpwith a POST param ofnidset to a numeric value out of range i.e.> int(11).This causes a PDOException and could be used as an attack vector if repeatedly called to try and take down a site.
Furthermore, per the minimal Drupal bootstrap in this file, no error would ever be sent to watchdog.
Proposed resolution
- do early exit when not integer NID is passed
- add try/catch to prevent any data send on error
Remaining tasks
Agree that this changes enough and commit
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #94 | 2616330-94.patch | 3.49 KB | andypost |
| #94 | interdiff.txt | 3.02 KB | andypost |
Issue fork statistics-2616330
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
catchI don't agree this is a DOS as such, but I have seen load balancers configured to take web servers out of rotation when they return enough 5** errors, and this would allow you to trigger that condition.
Comment #4
dave reidComment #5
dawehnerSo I'm wondering whether we should use some form of tokens to validate that the input is valid in any way?
Comment #6
catchThat'd be tricky since this is used for anonymous users.
Could possibly hash nid + site key or similar - it won't stop anyone adding fake entries, but it'll mean they were only able to add them if at some point someone visited the node?
Comment #8
mallezieDoes anyone has an idea here how to tackle this.
My first thought would be an extra check to determine range of IDs. However this add's an extra query.
Comment #9
catchIt just needs to check that the integer is small enough to fit in the database column, not that it's within the range of nids on the site.
Comment #10
mallezieLet's try this. This is probably simplest approach to do, not sure it's the best.
Added two tests to demonstrate the problem.
Comment #12
mallezieBack to needs review.
Comment #13
alexpottThat's not the maximum size of int(10)... see https://dev.mysql.com/doc/refman/5.5/en/integer-types.html
The largest size is 4294967295.
Comment #14
mallezieThanks!
Adjusted the patch.
I'm just not sure if it wouldn't be better to somehow define the value somewhere as a constant.
Comment #16
mallezieThis should probably better start against 8.3.x. Sorry for the noise
Comment #18
mallezieComment #19
dawehnerIs there a reason we cannot just use the default guzzle client but rather have to setup custom options? I'm curious here.
Note: you can also use
['timeout' => ], guzzle maps that internally.Comment #20
mallezieNope no reason, but i'll try to refactor it. (won't be today however)
only reason now is copy paste from StatisticsAdminTest
Note to self, if refactoring this works, file followup for StatisticsAdminTest
Comment #21
mallezieAdjusted the client declaration in the new test and created #2785997: Clean up guzzle declaration in core tests to refactor the current tests.
Comment #22
dawehnerTo be honest I don't get in the first place, why we need a timeout :)
IMHO we need some kind of assertions ...
Comment #23
mallezie@dawehner thanks!
I thought about the assertions as well. Problem is i have no idea what to assert.
We're testing we're not getting a PDO exception.
i'll update the timeout, it's also in tehe other tests, thats why i added it here.
Comment #25
dawehnerWell, we could check whether we have the expected node views recorded.
Comment #26
mallezieI'm gonna retest this one, strangely this works locally.
Comment #27
mallezieAnd in the meantime added the remarks from dawehner.
Comment #30
mallezieSeems the guzzle adjustments still fail here but work locally. The other two fails seems a problem in head.
Comment #31
mallezieStill no idea why the default guzzle instantiation fails, and the 'drupal' one works on the testbot. Both work locally.
Let's not hold this patch up on that? And fix that in #2785997: Clean up guzzle declaration in core tests ?
Adjusted back to drupal initialization.
Comment #33
mallezieOk that was a wrong patch ;-)
Comment #34
dawehnerYeah, that's indeed the right solution. You can shortcut that by using
\Drupal::httpClient();Comment #35
mallezieSo only thing left here, is adjust declaration in test from
$this->client = \Drupal::service('http_client_factory')->fromOptions();
to
$this->client = \Drupal::httpClient();
Comment #36
mallezieAnd adjusted this, i think this is ready now.
Comment #37
mallezieForgot an unused use statement
Comment #38
dawehnerCan't we use
$this->buildUrl()instead?Comment #39
timmillwoodbased on #38
Comment #41
stefank commentedHere is a patch against 8.4.x-dev branch with some coding standard changes and changes based on #38.
Comment #42
timmillwoodThanks @stefank, great to see this coming back after 3 months, sorry to have to push it to needs work.
I wonder if we could do this with a kernel test, if not it should be a browser test and not a simpletest based test.
Comment #43
stefank commentedSure, here is a browser test, not sure if its right though.
Comment #44
stefank commentedComment #45
dawehnerIt would be nice to document why this validation is applied here. (Otherwise we get a DB exception)
Comment #46
stefank commentedWould something like that be sufficient?
Comment #47
mallezieNit: Checks if posted nid is in range of valid integers and prevent a PDOException.
Lowercase P, and an 'a' before PDOException. But look corrects according to me.
Comment #48
stefank commentedCool, @mallezie thanks for that.
Sorry, somehow managed to upload the patch twice.
Comment #49
timmillwoodNeed a space after
//, and no more than 80 characters per line. So just dropPDOException.down to the next line.Comment #50
stefank commentedThanks @timmillwood. That is updated.
Comment #51
timmillwoodLooks good
Comment #52
dawehnerNice!
Comment #53
alexpottSo the number chosen looks great...
But I have a couple of concerns. One this value is different on postgres :( Here's a similar test on a postrges db.
And the other concern is that the test added should be fail if we update the node table to hold larger integers so we know we need to update statistics.php if we do that.
Comment #54
alexpottSo thinking about this some more
How can this take down a site? I've looked at the original issue on security.drupal.org and there is no supporting evidence of this. Yes a user can generate an error by manipulating a URL - is that a problem? If it is then perhaps we need to catch exceptions here. However, I've confirmed that if you've got error logging set to not display error messages all you see a 500 with a message...:
I'm not sure that we even need to fix this.
Comment #55
dawehnerThere is this usual argument that once you have a 500 your reverse proxy believes your site is down. I'm not sure at the moment whether this is per path or just for the entire domain. On the other hand this would probably just make the statistics to no longer appear.
In general I believe a 500 would cause less resources than a database insert.
Comment #58
wim leersIf this were the case then a single 500 response on d.o would bring all of d.o down. I'm 99.9999% certain that the reverse proxy will not conclude that the entire site is down. (If it happens for GET requests on the frontpage, that's a different story.) I'm 80% certain that most reverse proxies only consider a site to be down when their requests to origin time out.
True. That would make this a scalability rather than a security improvement issue.
OTOH, what do we really gain from letting the DB to attempt this query? The whole point of this controller is to receive requests from anonymous users to track page views. It's totally possible to DDoS Drupal by sending millions of requests to this. Similarly, you can bypass Drupal's page cache by providing a random URL query string. These are not things Drupal can protect against, that's up to the layer above it.
So, I agree with @alexpott:
Comment #59
wim leersOh, looks like @catch has a different experience — quoting him from #2:
Let's get his thoughts on this, also because he created this issue.
Comment #60
dawehnerIt is not an issue when you customize your settings. A better alternative for the uptime of your site is a dedicated script. Individual 500s then don't kill your site.
Comment #61
alexpottIf we want to stop this page throwing 500s that might be reasonable. Whilst I'm not sure that the site being taken down is a good argument I think saying that a script like this should not present an error ever might be. Therefore catching exceptions is what we should do rather than trying to filter because we can't get that right for the different databases we support.
Comment #62
wim leers#61 sounds great!
First, rebasing.
Comment #64
wim leersThis implements #61.
Comment #66
dawehnerDo you mind adding back in the test?
Comment #67
Anonymous (not verified) commentedSorry for the stupid question, but why does the internal script collect statistics on external queries at all?
It would be better to do this on sever side just before sending the render page:
Comment #68
yogeshmpawarRerolled the patch against 8.6.x branch because its failed to apply.
Comment #69
shardulagg commentedadded comments with changes
Comment #70
timmillwood@vaplas Re: #67 - This used to be a server side request back in early releases of Drupal 7 and before, but it meant you couldn't use it with reverse proxy caching like Varnish, because only the first view after a cache clear would get counted. The AJAX request was put in to bypass the caching and count all views.
Comment #71
timmillwoodRe #68 @Yogesh Pawar
Incomplete comment
Re #69 @shardulagg
Lots of odd white space added.
Missing capital letters and full stops / periods.
Comments longer than 80 characters.
Comment #72
yogeshmpawarUpdated patch & interdiff as per comment #71
Comment #73
timmillwoodAs suggested in #66, can we have the test back?
I think we can remove this comment.
The options array looks like it can be removed?
Comment #74
Anonymous (not verified) commented@timmillwood, thank you very much for your help in this matter. Clever mechanism hiding here. I knew it!) Perhaps it is worth discussing this change, but this is certainly beyond the scope of this issue.
Additional interdiff file for the main patch with
git diff -wwill make review the patch more readable (now shifting all lines creates unnecessary noise).Also, the decision taken does not coincide with the IS / title of issue. Because we don't validate nid, but just catch exceptions. Or not?
Comment #75
andypostThis is bug and why test is lost? #66 said it as well
this one from #62
better to expand options array to next lines
better to tell why... do nothing
Comment #76
mohit_aghera commentedAdding removed test cases again. see #66
Fixed issues mentioned by @andypost
Need some more help in refining catch block description further.
Comment #86
smustgrave commentedStarting at #76 not sure why the test was added to the file module? The namespace even says statistics so I went ahead and moved that.
Updated phpcs errors and deprecated calls.
Comment #88
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #89
pooja saraah commentedFixed failed commands on #86
Attached patch against Drupal 10.1.x
Comment #90
_pratik_Comment #91
_pratik_Comment #92
alexpottIn #61 we agreed to not validate the range. It's not possible to set a value for all DB types supported by core. The correct patch to be re-rolling is #68 - unfortunately #69 added this back in without explanation and after it was decided that filtering by number is incorrect.
Comment #93
smustgrave commentedComment #94
andypostAddress #92 by removing extra range filtering which is database dependent and will need real controller to check further access (currently it using config setting) and re-titled
Interdiff vs #89 - we should not add theme to base test as child ones must care
Comment #95
andypostHope this kind of optimization fits
I see no reason to have 2 requests because of bug #2613044: Requests are pushed onto the request stack twice, popped once
Comment #96
smustgrave commentedFeel bad I was part of that carrying the bad approach forward.
Can we update the issue summary for the proposed solution and remaining tasks?
Comment #97
andypostUpdated to the last patch and #61
Comment #98
smustgrave commentedThanks for the quick turnaround.
So reading the proposed solution compared to the patch I see it now.
Ran the test locally just to make sure it failed but it passes without the fix. So think they need to be tweaked.
Comment #99
quietone commentedStatistics is approved for removal. See #3266457: [Policy] Deprecate Statistics module in D10 and move to contrib in D11
This is now Postponed. The status is set according to two policies. The Remove a core extension and move it to a contributed project and the Extensions approved for removal policies.
It will be moved to a contributed Statistics project once the project is created and the Drupal 11 branch is open.
Comment #101
quietone commentedComment #102
fgmReworded IS to account for changes since D8.
Comment #104
fgmPinging @catch since you were the original issue poster. @mallezie @stefank since you did the most patches before the split.
Comment #105
fgmDaniel's suggestion in #5 is relevant in the "transitions tracking" part of the plan #3436823: Plan for Statistics 1.x.
Comment #106
fgmRegarding the nid validation : the maximum check was removed for DB independence, but can we agree we should really keep the
min_range => 1check ? I do not think there is any case in which a nid can be zero or negative, is there ?Although it will not be inserted because the column is marked
UNSIGNED, applying the check in the filter will save a roundtrip to the DB in that case.Comment #108
fgmMerged 9 years later. Thanks everyone.