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

Issue fork drupal-2348931

Command icon 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

greggles’s picture

StatusFileSize
new997 bytes

Here'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.

greggles’s picture

Status: Active » Needs review

I know how to use the status, I swear.

Status: Needs review » Needs work

The last submitted patch, 1: 2348931_let_mysql_prepare.patch, failed testing.

damien tournoud’s picture

I cannot recommend this:

  • Prepared statements are actually extremely expensive, because take *two* round trips instead of one; most queries are not going to be reused anyway, so this they are prepared in pure waste;
  • I can confirm that the C MySQL prepared statement API only supports single statement queries, but I don't see why this is not just a bug. The (very sparse) protocol documentation doesn't even mention this limitation.
greggles’s picture

I 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.

pwolanin’s picture

Priority: Normal » Critical

If 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

moshe weitzman’s picture

Priority: Critical » Major

We could clearly release without this. Splitting the difference.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new3.45 KB

The 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.

Status: Needs review » Needs work

The last submitted patch, 9: 2348931.9.patch, failed testing.

alexpott’s picture

StatusFileSize
new3.03 KB

And now without my .htaccess file.

alexpott’s picture

Status: Needs work » Needs review
alexpott’s picture

StatusFileSize
new4 KB

Doh - forgot the whole point of the patch ... the changes in Connection.php. Third time lucky.

Status: Needs review » Needs work

The last submitted patch, 13: 2348931.13.patch, failed testing.

Crell’s picture

Thanks, Greg!

I don't know why we'd be getting that error about unbuffered queries. We already are setting buffered queries to true:

$connection_options['pdo'] += array(
      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
      // So we don't have to mess around with cursors and unbuffered queries by default.
      \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
      // Make sure MySQL returns all matched rows on update queries including
      // rows that actually didn't have to be updated because the values didn't
      // change. This matches common behavior among other database systems.
      \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
      // Because MySQL's prepared statements skip the query cache, because it's dumb.
      \PDO::ATTR_EMULATE_PREPARES => 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.

ircmaxell’s picture

I 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.

pwolanin’s picture

Title: Prevent executing multiple database queries in a single PDO execute » Use native MySQL statement preparation via PDO
Issue summary: View changes
catch’s picture

Issue summary: View changes

The 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.

damien tournoud’s picture

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.

That'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.

mpdonadio’s picture

Issue tags: +Needs reroll

Patch no longer applies. I'll reroll today.

mpdonadio’s picture

Issue tags: -Needs reroll
StatusFileSize
new3.89 KB

Rebase + one manual merge, I think due to some of the cache tag changes that got in recently.

Crell’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 22: use_native_mysql-2348931-22.patch, failed testing.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

colan’s picture

I 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:

Some servers allow you to cache and reuse query identifiers for multiple prepared queries to reduce round trips.

Could this be a solution for dealing with the caching issue?

klausi’s picture

Issue summary: View changes

As 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.

mpdonadio’s picture

Issue tags: +Needs reroll
mpdonadio’s picture

Version: 8.2.x-dev » 8.3.x-dev
Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new819 bytes

OK, as a starting point let's do a fresh run against 8.3 w/ `\PDO::ATTR_EMULATE_PREPARES => FALSE` to see what happens now.

Status: Needs review » Needs work

The last submitted patch, 30: 2348931-30.patch, failed testing.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mpdonadio’s picture

Status: Needs work » Needs review
Issue tags: +DrupalCampNJ2017, +needs profiling
StatusFileSize
new1007 bytes
new963 bytes

Experiment.

Status: Needs review » Needs work

The last submitted patch, 33: 2348931-33.patch, failed testing.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

prudloff made their first commit to this issue’s fork.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

prudloff’s picture

Failing tests revealed these changes:

  • We can't reuse the same placeholder multiple times in the same query anymore.
  • Some queries now fail earlier (in prepare() instead of execute()) so the failure does not trigger a StatementExecutionFailureEvent.
  • Values are not casted to string automatically anymore. So for example using CONCAT_WS() on a string and an int will cause "Illegal mix of collations" error unless we use CONVERT() to cast explicitly.
prudloff’s picture

I 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.

prudloff’s picture

Issue summary: View changes
prudloff’s picture

Status: Needs work » Needs review

I reused this module to do some basic profiling: https://git.drupalcode.org/sandbox/geerlingguy-2308763/-/tree/2.0.x?ref_...

With patch:

Results for 100 test runs
query() Simple - 0.220046 ms
select() Simple - 0.243607 ms
EntityQuery Simple - 1.092334 ms
query() with Joins - 0.848203 ms
select() with Joins - 0.904932 ms

Without patch:

Results for 100 test runs
query() Simple - 0.219357 ms
select() Simple - 0.233397 ms
EntityQuery Simple - 1.066444 ms
query() with Joins - 0.823562 ms
select() with Joins - 0.868673 ms

But we might need more advanced profiling?

mondrake’s picture

Component: database system » mysql db driver
Related issues: +#3576212: Fix errors when using unbuffered mysql queries
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The 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.

prudloff’s picture

Status: Needs work » Needs review

I merged the latest main.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The 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.

prudloff’s picture

Status: Needs work » Needs review
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The 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.