Problem/Motivation
Postgres PDO Driver uses native statement preparation by default. PDO MySQL uses emulated prepares by default. Native prepares are much more robust against SQL injection, so should be used if at all possible.
As a side effect, MySQL native prepares only support preparing one SQL statement at a time.
In Drupal 6 and below a sql injection vulnerability/attack was limited in what it could achieve by the surrounding statement. For example, if there were a select statement it could only do selects and maybe union data, but it couldn't be closed with a ; and then have an update executed. An update of one table could only update that one table.
Drupal 7, with adopting PDO, makes it possible to close off the first query and execute a second query. This makes a sql injection vulnerability a much riskier thing.
Risks/benefits:
* Benefit: not executing multiple queries.
* Risk/Benefit (?): statement preparation (including security related elements) is moved to mysql instead of php. I don't know how that might impact any code we have related to security. It seems reasonable to me to trust the mysql escaping as much as the PHPPDO escaping.
* Risk: validation of statements, including exceptions about a column or table that is missing, will be thrown during prepare instead of execute. We may have some code that relies on
* Risk: additional TCP round trip time when doing a prepared statement query. This could add up in cloud settings where the connection to the database server takes longer.
Some research:
* the original thread that pointed me to this option
* A pretty solid stack overflow thread on php-pdo-mysql and impact
* php documentation on the attribute - pretty limited
I'm purposefully filing this in public. Even though it is related to security it's about hardening. This issue doesn't expose any vulnerabilities in Drupal and a change like this deserves broad review.
Steps to reproduce
Proposed resolution
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-2348931
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 #1
gregglesHere's a d8 version. Let's see what testbot says.
It probable needs benchmarking.
I'll post a d7 patch in a bit and steps to show how this adds security when I do that.
Comment #2
gregglesI know how to use the status, I swear.
Comment #4
damien tournoud commentedI cannot recommend this:
Comment #5
gregglesI agree it needs to be benchmarked, no doubt there. It could be slower, but a benchmark will show that.
Regarding whether it's a bug or feature that only a single query gets executed - can you expand on the scenarios where you like to send multiple queries in a single statement? I haven't seen any myself.
Comment #6
pwolanin commentedSimilar: http://stackoverflow.com/questions/2802298/can-phps-pdo-be-limited-to-a-...
Comment #7
pwolanin commentedIf it's possible to do this, we should consider it critical, since Drupal 7 is actually much more vulnerable when there is an SQL injection than Drupal <= 6 becuase of the switch to PDO
Comment #8
moshe weitzman commentedWe could clearly release without this. Splitting the difference.
Comment #9
alexpottThe actual error is "SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll(). Alternatively, if your code is only ever going to run against mysql, you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.: SAVEPOINT savepoint_1; Array"
This is being caught and lost in MenuRouterRebuildSubscriber::menuLinksRebuild().
I think the patch attached will get Drupal to install - only posting as it might point people towards the correct fix - as it most certainly is not.
Comment #11
alexpottAnd now without my .htaccess file.
Comment #12
alexpottComment #13
alexpottDoh - forgot the whole point of the patch ... the changes in
Connection.php. Third time lucky.Comment #15
Crell commentedThanks, Greg!
I don't know why we'd be getting that error about unbuffered queries. We already are setting buffered queries to true:
5 years ago when we were developing this system MySQL 5.1 mattered. In 2015 I don't care about MySQL 5.1 at all, so the query cache issue is a non-issue. I am open to switching back to native prepares if the performance hit isn't too great. That will require benchmarks.
I'm reasonably sure we're never deliberately shoving multiple queries into one query string, so removing the ability to do so is not a notable downside.
Comment #16
ircmaxell commentedI disagree with the premise of this issue (but not the patch).
Disabling emulated prepares is not about preventing executing multiple queries in a single exec() call. It's about security at a far more fundamental level.
It sends the data separately from the query and hence is 100% immune from injection. Not just the "should be" of emulated prepares or using `->quote()`, but 100% immune. At both a theoretical and practical level.
Prior to MySQL 5.1.21, there were issues such as query cache. Today, those are fixed. D8's minimum requirement was raised to 5.1.21 for that exact reason: https://www.drupal.org/node/1856262
The performance difference is trivial (one TCP round trip per query). If that is significant for you usage, that means you're executing too many queries in the first place.
Please don't compromise security overall because you're making design tradeoffs elsewhere.
Comment #17
pwolanin commentedComment #18
catchThe primary bottleneck for Drupal performance has not been the database since Drupal 6, or not in core anyway.
Drupal 7 was (nearly) able to serve authenticated page requests from a warm cache with zero database hits if using the memcache cache backend. The only one we didn't eventually fix was menu_get_item(), but that effort has been revived recently too - #1234830: Revert cache_menu patch removal, add a $conf setting instead (was: cache_menu: huge table size).
I think it's reasonable to have a critical issue open to ensure 8.x doesn't regress from 'at most one non-cache/state database hit with a warm cache on simple pages'.
For cold caches, again in Drupal 7, path alias whitelisting, entity multiple loading and menu link rebuilding all helped to cut down a lot of queries.
Drupal 8 should reduce the number of select queries on cache misses even more, because a lot of that has been replaced by YAML parsing. If this isn't true, that's also worth a critical issue to figure out what's wrong (/me looks sideways at link generation).
Actual sites with contributed modules are a completely different matter, but CMI/state/entity API may help with that too in 8.x on cache misses, and render caching definitely with warm caches.
So from a performance standpoint I'd like to see the following:
1. How many non-cache, non-state queries do we execute with warm caches, and can we get rid of all them?
2. How many select queries do we execute with cold caches (don't care about TCP overhead for insert/update that's bad anyway), and can we reduce them/make them multiple/offload?
3. How much of a regression is the native prepared statement in real terms regardless of #1/2?
We should know all of this before committing a patch here, not sure the answer to any of these should block it - just might mean prioritising being a bit lighter on the database.
Comment #19
pwolanin commentedsee also: #2388255: (followup) Limit PDO MySQL to executing single statements if PHP supports it
Comment #20
damien tournoud commentedThat's all but insignificant, especially in cloud settings. If you have 1ms round trip between your PHP application server and your MySQL server (which is on the low side in cloud settings), and do 10 queries per page (which is really nothing, even outside of Drupal), you add 10ms to your page processing time.
Comment #21
mpdonadioPatch no longer applies. I'll reroll today.
Comment #22
mpdonadioRebase + one manual merge, I think due to some of the cache tag changes that got in recently.
Comment #23
Crell commentedComment #27
colanI got here from reading On the (in)security of popular open source Content Management Systems written in PHP: Drupal Almost Offers Prepared Statements.
Earlier in the article, they say:
Could this be a solution for dealing with the caching issue?
Comment #28
klausiAs far as I understand it we only allow one statement per query. PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE in our mysql driver, see #2388255: (followup) Limit PDO MySQL to executing single statements if PHP supports it.
So now this comes down to the round trip discussion where query and data is sent separately. An updated working patch would help here to start the profiling.
Removed the MySQL version risk from the issue summary, we depend on MySQL 5.5 for some time now.
Comment #29
mpdonadioComment #30
mpdonadioOK, as a starting point let's do a fresh run against 8.3 w/ `\PDO::ATTR_EMULATE_PREPARES => FALSE` to see what happens now.
Comment #33
mpdonadioExperiment.
Comment #50
prudloff commentedFailing tests revealed these changes:
Comment #51
prudloff commentedI had a hard time understanding why StandardPerformanceTest needed to be changed so I'm writing the explanation for reference:
The request in DatabaseStorageExpirable::getMultiple() fails because the table does not exist (and the exception is caught).
With emulated prepares, it fails at execute so the query is still collected by collectPerformanceData().
With native prepares, it fails at prepare so the query is not collected.
Comment #52
prudloff commentedComment #53
prudloff commentedI reused this module to do some basic profiling: https://git.drupalcode.org/sandbox/geerlingguy-2308763/-/tree/2.0.x?ref_...
With patch:
Without patch:
But we might need more advanced profiling?
Comment #54
mondrakeComment #55
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #56
prudloff commentedI merged the latest main.
Comment #57
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #58
prudloff commentedComment #59
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.