Problem/Motivation

Drupal core is is inconsistent about how placeholder tokens are generated, and in some cases the short hashes used may lead to collisions and incorrect output

In Render::createPlaceholder()

    $attributes['token'] = hash('crc32b', serialize($placeholder_render_array));

This is a major bug. crc32b is not very collision resistant, for example:

e.g.

hash('crc32b', "penetration"); == hash('crc32b', "prepituitary");

Proposed resolution

Replace uses of crc32b or sha1 with the standard Drupal core hash algorithm exposed via \Drupal\Component\Utility\Crypt::hashBase64()

Remaining tasks

User interface changes

n/a

API changes

n/a

Data model changes

n/a

Comments

pwolanin created an issue. See original summary.

googletorp’s picture

Issue tags: +Performance

We should make sure, that this doesn't have any negative performance implications.

jhedstrom’s picture

Quick benchmarking shows sha256 to be quite a bit slower:

crc32b: 6.5723259449005
sha256: 16.812311887741
<?php
function microtime_float()
{
      list($usec, $sec) = explode(" ", microtime());
      return ((float)$usec + (float)$sec);
}

$string = file_get_contents('http://www.drupal.org');
foreach (['crc32b', 'sha256'] as $method) {
  $start = microtime_float();
  for ($i = 0; $i < 100000; $i++) {
    $hash = hash($method, $string);
  }
  print $method . ': ' . (microtime_float() - $start) . "\n";
}
jhedstrom’s picture

Note, I have no idea if this is a realistic benchmark in terms of string size, or times this would be called in a page load.

pwolanin’s picture

sha2 is certainly slower, though the code in question probably hashes a much smaller piece of data, and shouldn't run many times per page.

Locally on a shorter string I see about 1.7 µs vs 4.9 µs per call for crc32b vs sha512

dawehner’s picture

Well, someone should write a patch and then see whether it actually matters and whether it sums up enough FOR cached pages, where it actually matters.

jhedstrom’s picture

Status: Active » Needs review
StatusFileSize
new1.61 KB

I found 2 places this might be an issue, here's the patch.

Status: Needs review » Needs work

The last submitted patch, 7: 2569119-07.patch, failed testing.

The last submitted patch, 7: 2569119-07.patch, failed testing.

jhedstrom’s picture

Status: Needs work » Needs review
StatusFileSize
new1.53 KB
new3.13 KB

Fixes tests.

jhedstrom’s picture

Did some basic benchmarking using 10 nodes on the homepage (60 total nodes), logged in as user 1.

With cold caches, time spent in PlaceholderGenerator::createPlaceholder():

Current 8.0.x: 152 microseconds
With patch: 166 microseconds

So, not much change. With warmed and fully primed caches, there were 0 calls to this method.

pwolanin’s picture

Looks fine. This will use hex values - if we want to make the sting shorter we could base64 encode the binary string, but that's not essential:

$token = base64_encode(hash('sha256', serialize($stuff), TRUE));
yareckon’s picture

Status: Needs review » Reviewed & tested by the community

Is there anything more to do / discuss here, or can we mark this rtbc? Seems rather important to guarantee no collisions on placeholders.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 10: 2569119-10.patch, failed testing.

jhedstrom’s picture

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

Re-roll.

pwolanin’s picture

Status: Needs review » Reviewed & tested by the community

Looks good, and gives us consistency at least.

catch’s picture

Status: Reviewed & tested by the community » Needs review

So sha1 is just pretending and we should not use it anywhere.

crc32b does not pretend, and I think all of our use-cases in core really are not going to have hash-collisions, however there is a chance that people use crc32 via bad copy and paste of the pattern and crc32 is lol in PHP. Also you have to carefully review all the places it's used to see if they're collision-sensitive or not which is not ideal.

sha256 looks fine, but there is no documentation as to why we're picking that over anything else.

So I think what I'd prefer to do here is add a specific helper for non-cryptographic hashing, which we use in all these places. Then we can use sha256 for now but if we think of something else, change that centrally, and contrib can then rely on it too.

pwolanin’s picture

Status: Needs review » Needs work

@catch. I disagree with your premise that we should distinguish different types of hash usage. You are asking contrib authors to analyze uses of hashing to understand which ones have security implications or not. This is really not viable.

However, I think you are correct that we should be consistent. In this case we should have used Crypt::hashBase64() since that is already central and is what we want everyone to use.

marvin_b8’s picture

StatusFileSize
new3.49 KB

@pwolanin you prefer something like this right ?

marvin_b8’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 19: 2569119-19.patch, failed testing.

The last submitted patch, 19: 2569119-19.patch, failed testing.

marvin_b8’s picture

Status: Needs work » Needs review
StatusFileSize
new3.69 KB
new1.57 KB

sry the interdiff is not a patch ....

Status: Needs review » Needs work

The last submitted patch, 23: interdiff-2569119-19-22.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review

The actual patch is passing

Status: Needs review » Needs work

The last submitted patch, 23: interdiff-2569119-19-22.patch, failed testing.

marvin_b8’s picture

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

same patch #23

effulgentsia’s picture

Title: Render class should use a sha2 hash not crc32b hash for placeholder tokens » PlaceholderGenerator should use a sha2 hash not crc32b hash for placeholder tokens
Issue tags: +Security improvements, +rc target triage
Related issues: +#2562341: Add non-HTML placeholder generation to PlaceholderGenerator or a new service
xjm’s picture

Issue tags: -rc target triage

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.

xjm’s picture

Issue tags: +Triaged core major

Discussed with @alexpott, @Cottser, @lauriii, and @joelpittet at DrupalCon New Orleans. We agreed that this is a major bug since there is a small/theoretical but non-infinitesimal risk here of accidental disclosure in case of a placeholder collision.

Do placeholders end up in the cache? If so, we probably want an empty update hook to force a cache invalidation for this change?

Aside from that question, I think the current patch just needs review. I think #18 somewhat addresses @catch's concerns (and that makes sense to me).

wim leers’s picture

Queued #27 for re-testing, because it is A) more than 6 months old, B) predates the addition of BigPipe to Drupal core, which I am pretty certain will also need some tests to be updated.

wim leers’s picture

Title: PlaceholderGenerator should use a sha2 hash not crc32b hash for placeholder tokens » PlaceholderGenerator should use Crypt::hashBase64(), not hash('crc32b') for placeholder tokens

Status: Needs review » Needs work

The last submitted patch, 27: 2569119-22.patch, failed testing.

marvin_b8’s picture

Version: 8.1.x-dev » 8.2.x-dev
Status: Needs work » Needs review
StatusFileSize
new22.2 KB

Status: Needs review » Needs work

The last submitted patch, 35: 2569119-34.patch, failed testing.

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

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now 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.

csheltonlcm’s picture

StatusFileSize
new22.19 KB

I've rerolled the patch from #35 to apply cleanly on 8.3.x. It doesn't pass testing, though.

csheltonlcm’s picture

Status: Needs work » Needs review
StatusFileSize
new29.71 KB

Fixed the remanining bugs in core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php and core/modules/big_pipe/src/Tests/BigPipeTest.php related to the hash function.

Still getting some other test failures for the big pipe module, but it could just be something with my db setup - for example, I keep getting "Incorrect string value..." because of an em-dash in core/modules/big_pipe/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php (which ultimately gets inserted into simpletest.function, an ascii column).

Also getting an error "Data too long for column 'function' at row 6" - the offending value is

'Drupal\Tests\big_pipe\Unit\Render\BigPipeResponseAttachmentsProcessorTest->testHtmlResponse with data set "all official attachment types, with random assigned values, even if technically not valid, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator"()'

Status: Needs review » Needs work

The last submitted patch, 39: 2569119-38.patch, failed testing.

csheltonlcm’s picture

Status: Needs work » Needs review
StatusFileSize
new34.43 KB
dawehner’s picture

I'm wondering whether the right approach here would be to introduce a InsecureHash method which allows people to create a hash, which is not used in the area of security, but rather just for hashing stuff, like cache or here these placeholders. In there we could then either use crypt or just use some of the other ones, but at least we never have to argue about it anymore.

csheltonlcm’s picture

Yeah, I was thinking that as well - I think that might actually be covered by #2562341: Add non-HTML placeholder generation to PlaceholderGenerator or a new service, but I'm not entirely sure

pwolanin’s picture

@dawehner - no, I do not think that is a viable approach and there is no reason to prefer other hashes. Please don't try to micro-optimize away the auditability of Drupal core.

Since this is generating a hash of a fixed string in \Drupal\Core\Form\FormBuilder::prepareForm, we can just hard-code the hashed string?

Not sure what the value is of even using a hash there?

-      $placeholder = 'form_action_' . hash('crc32b', __METHOD__);
+      $placeholder = 'form_action_' . Crypt::hashBase64(__METHOD__);

__METHOD__ is the string 'Drupal\Core\Form\FormBuilder::prepareForm'

pwolanin’s picture

StatusFileSize
new34.67 KB
new1.03 KB

Like so? Avoids a bunch of possible method calls on every page.

Leaves the hash call in the test to keep us honest, though not sure it really matters.

jhedstrom’s picture

This is looking good. But I thin since the scope now appears to be beyond just PlaceholderGenerator, the issue title and summary could use a quick updating.

pwolanin’s picture

Title: PlaceholderGenerator should use Crypt::hashBase64(), not hash('crc32b') for placeholder tokens » Use Crypt::hashBase64(), not hash('crc32b') os sha1 for placeholder tokens
Issue summary: View changes

updatng title and summary

pwolanin’s picture

Title: Use Crypt::hashBase64(), not hash('crc32b') os sha1 for placeholder tokens » Use Crypt::hashBase64(), not hash('crc32b') or sha1 for placeholder tokens

typo

jhedstrom’s picture

From #31:

Do placeholders end up in the cache? If so, we probably want an empty update hook to force a cache invalidation for this change?

I think at least some of these get cached, so an empty update hook should be added.

pwolanin’s picture

StatusFileSize
new35.12 KB
new458 bytes

ok, here it is

jhedstrom’s picture

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -683,8 +683,9 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
-      // https://www.drupal.org/node/2562341.
-      $placeholder = 'form_action_' . hash('crc32b', __METHOD__);
+      // https://www.drupal.org/node/2562341. The placholder uses a fixed string
+      // that is Crypt::hashBase64('Drupal\Core\Form\FormBuilder::prepareForm');
+      $placeholder = 'form_action_p_pvdeGsVG5zNF_XLGPTvYSKCf43t8qZYSwcfZl2uzM';

This bit is still really confusing to me. I wonder if that was supposed to originally be a form class method being hashed? That would at least vary the placeholder per-form...

Aside from that, I think this looks good.

jhedstrom’s picture

Status: Needs review » Reviewed & tested by the community

Assuming the code snippet in #51 is for better cachibility (as the code comment mentions), then I think hard-coding this is fine for now (since it is effectively hardcoded anyway given that the method doesn't change).

The last submitted patch, 38: 2569119-36.patch, failed testing.

dawehner’s picture

@dawehner - no, I do not think that is a viable approach and there is no reason to prefer other hashes. Please don't try to micro-optimize away the auditability of Drupal core.

I think you are coming from a total biased argument background :p
All I really just care about is readability, and by that better education about the problem space. Here is an example of what I thought about SecureHash::hash() vs Crypt::hashBase64()
This is for me all about the readability, nothing whatever historic fight you had in the past.

wim leers’s picture

Issue tags: -Security improvements

This does not improve security. It at most improves security perception, by not getting alarming results out of poor security audits.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I think we need a followup to add a coding standard to warn against the use of the hash function and to tell people to use Crypt::hashBase64() as the preferred method of generating a hash.

+++ b/core/modules/system/system.install
@@ -1728,5 +1728,12 @@ function system_update_8201() {
 /**
+ * Clear caches due to changes to render placeholders.
+ */
+function system_update_8301() {
+  // Empty update to cause a cache rebuild.
+}
+
+/**

Should be a post-update - given we just added one we don't need to add another - however we should add comments about all the stuff it is expected to cause a cache clear for. And change it's name system_post_update_clear_twig_cache to system_post_update_clear_8_3

I spent sometime pondering the feedback of @dawehner and @catch about adding a specific method for non-cryptographic hashing. Initially I agreed with them - but then I noticed that https://secure.php.net/manual/en/function.hash.php is part of PHP's cryptography extension system. I don't think we add clarity by mudding the water and making people think about cryptographic hashes and non-cryptographic hashes.

pwolanin’s picture

@alexpott - yes, I 100% agree I had previously suggested such a coding standard, and also agree that we should simply use one strong hash for everything since people have a very difficult time understanding the correct use cases.

Also - path needs a re-roll
CONFLICT (content): Merge conflict in core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php

pwolanin’s picture

Status: Needs work » Needs review
StatusFileSize
new35.14 KB

Here's a re-roll which move the hash change from core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php to core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php

NR for testbot.

alexpott’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TwigExtensionPass.php
@@ -26,7 +27,7 @@ public function process(ContainerBuilder $container) {
-    $container->setParameter('twig_extension_hash', hash('crc32b', $twig_extension_hash));
+    $container->setParameter('twig_extension_hash', Crypt::hashBase64($twig_extension_hash));

I just don't think this change is right. Over in #2606772: Long Twig cache directories can cause failures on some filesystems we've discussed filename lengths at length. This change adds nothing but pain for windows users.

alexpott’s picture

Also I think #59 points to the need for a short hash function in Crypt based on hashBase64 because this is not the only place where using lengthy hashes is non-sensical.

pwolanin’s picture

@alexpott - you make a short hash by taking a substring, and in general you don't want to do that. Let's not encourage it via core APIs. Usually want the 256 bits and don't want to make people guess about what an appropriate length. Core APIs should be secure as much as possible without thinking about it.

If it's really a problem here we'll change the other patch to throw a requirement error at a depth of 80 instead of 100. they are both arbitrary numbers.

This code is actually really dumb and should not need to be using a hash here at all in the file name - it could as well be using the output of uniqid() if we had access to compare to the prior hash. That's the missing piece.

pwolanin’s picture

StatusFileSize
new40.26 KB
new6.02 KB

After extensive discussion with alexpott in IRC, here's a tweak to the way we generate the cache file prefix - it will use uniqid() which is 13 chars instead of the current 8, so much less of a change than using a 44 char hash and also (to my mind) more semantically correct as well as very collision resistant.

Also fixes a crc call in core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php

Status: Needs review » Needs work

The last submitted patch, 62: 2569119-62.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
StatusFileSize
new41.96 KB
new3.9 KB

Fix to make the current prefix accessible from the TwigEnvironment

pwolanin’s picture

StatusFileSize
new42.08 KB
new824 bytes

Code comment fix.

alexpott’s picture

  1. +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
    @@ -66,6 +83,16 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension
       /**
    +   * Get the cache prefixed used by \Drupal\Core\Template\TwigPhpStorageCache
    +   *
    +   * @return string
    +   *   The file cache prefix, or empty string if the cache is disabled.
    +   */
    +  public function getTwigCachePrefix() {
    +    return $this->twigCachePrefix;
    +  }
    

    This seems unnecessary? If this is just for a test then we should use reflection. However - the prefix was publicly available before as it was stored as a container parameter and therefore available to all services. So maybe this is a good enough replacement. That said, I think we should consider the twig cache directories as internal to the twig system so I'd rather come up with a valid non-theoretical use-case first.

  2. +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
    @@ -158,9 +159,8 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name,
    -      // Hash the settings into a string. crc32 is the fastest way to hash
    -      // something for non-cryptographic purposes.
    -      $parts[] = hash('crc32b', serialize($settings));
    +      // Hash the settings into a string.
    +      $parts[] = Crypt::hashBase64(serialize($settings));
    

    This method is one of the most important for Drupal performance - we should definitely have a look at the impact of this change on a site with a complex layout involving lots of bundles with lots of fields.

pwolanin’s picture

StatusFileSize
new42.2 KB
new968 bytes

@alexpott - actually - why are we even using a hash here? There is no need to limit the length and an array access basically runs a hash on the string input.

Actually - array access is quite a bit slower and memory usage higher with long strings - so if the serialized settings could be long, that be a problem. However, if the serialized strings can be long we also want the hash to be strongly collision resistant (which crc32b is not).

Here's using a binary sha-256, since we don't need it to be URL safe or human readable. The speed difference is minor, takes roughly 1.2 µs vs 3.0 µs (cli with PHP 5.6 on a modest sized string).

As far as the method - I agree it's kind of an internal detail, and only really relevant for testing, but I don't think it's possible to do this via reflection since we are getting the instance out of the service container.

pwolanin’s picture

StatusFileSize
new42.18 KB
new1.04 KB

Ok, talked to alexpott in IRC - let's just try using the settings directly in the key using json_encode().

fabianx’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs change record

I think I would like a change record for this, besides that this looks RTBC to me.

I was first confused about the twig changes, but the uniqid() definitely makes sense.

One question though:

What happens in a race-condition of several competing requests to write their new uniqid() to the state?

I think in the worst we just write some cache items we will never need anymore, right?

alexpott’s picture

@Fabianx thanks for bringing up the race condition concern. I think you are right about there being a window. I also think you are right about the worst that will happen is some twig files will be written and used once. They would be cleaned up on a cache clear.

pwolanin’s picture

Added a draft change record at https://www.drupal.org/node/2833264 - let me know if that covers all that's needed.

dawehner’s picture

@pwolanin
I think more important the CR should contain the information that you should not use 'crc32b' or 'sha1' but rather Crypt::hashBase64

pwolanin’s picture

@dawehner - ok - that was already true, but we can certainly emphasize it.

pwolanin’s picture

@dawehner, this was already documented since Drupal 7 at https://www.drupal.org/docs/7/security/writing-secure-code-0/use-of-hash...

But I added another change record to emphasize it.

fabianx’s picture

Issue tags: -Needs change record

Thanks, pwolanin.

The reason why I think a change record is needed again (thanks for writing) is the many core contributors that did not use this best-practice (and getting that code committed).

There clearly was a lack of this mindset:

'even if this is not security related we need to use a strong hash regardless'

alexpott’s picture

StatusFileSize
new174.78 KB
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -158,9 +159,9 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name,
-      // Hash the settings into a string. crc32 is the fastest way to hash
-      // something for non-cryptographic purposes.
-      $parts[] = hash('crc32b', serialize($settings));
+      // Include the settings serialized as JSON as part of the key. The JSON is
+      // a shorter string than the serialized form, so array access is faster.
+      $parts[] = json_encode($settings);

This method is key for typed data performance. There are performance considerations on the issue but not any numbers that I think we should see. This method will be called for every typed data object used.

Doing a simple xhprof without and with the patch shows that on standard there's very little difference. Which is great so we're not dealing with a massive change. All the differences are well with the normal variation of repeated runs.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

So I think I've satisfied my performance concerns.

Committed 94f0aac and pushed to 8.3.x. Thanks!

diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index e0a1b58..b1c295d 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Core\TypedData;
 
-use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Plugin\Exception\PluginException;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;

removed unused use on commit.

  • alexpott committed 94f0aac on 8.3.x
    Issue #2569119 by pwolanin, marvin_B8, jhedstrom, csheltonlcm, alexpott...

Status: Fixed » Closed (fixed)

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