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
| Comment | File | Size | Author |
|---|---|---|---|
| #76 | Screen Shot 2016-12-15 at 10.06.14.png | 174.78 KB | alexpott |
| #68 | increment-2569119-67.txt | 1.04 KB | pwolanin |
| #68 | 2569119-67.patch | 42.18 KB | pwolanin |
| #67 | increment-2569119-67.txt | 968 bytes | pwolanin |
| #67 | 2569119-67.patch | 42.2 KB | pwolanin |
Comments
Comment #2
googletorp commentedWe should make sure, that this doesn't have any negative performance implications.
Comment #3
jhedstromQuick benchmarking shows sha256 to be quite a bit slower:
Comment #4
jhedstromNote, 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.
Comment #5
pwolanin commentedsha2 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
Comment #6
dawehnerWell, 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.
Comment #7
jhedstromI found 2 places this might be an issue, here's the patch.
Comment #10
jhedstromFixes tests.
Comment #11
jhedstromDid 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.
Comment #12
pwolanin commentedLooks 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:
Comment #13
yareckon commentedIs there anything more to do / discuss here, or can we mark this rtbc? Seems rather important to guarantee no collisions on placeholders.
Comment #15
jhedstromRe-roll.
Comment #16
pwolanin commentedLooks good, and gives us consistency at least.
Comment #17
catchSo 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.
Comment #18
pwolanin commented@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.Comment #19
marvin_b8 commented@pwolanin you prefer something like this right ?
Comment #20
marvin_b8 commentedComment #23
marvin_b8 commentedsry the interdiff is not a patch ....
Comment #25
pwolanin commentedThe actual patch is passing
Comment #27
marvin_b8 commentedsame patch #23
Comment #28
effulgentsia commentedSee also #2562341-4: Add non-HTML placeholder generation to PlaceholderGenerator or a new service.
Comment #29
xjmComment #31
xjmDiscussed 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).
Comment #32
wim leersQueued #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.
Comment #33
wim leersComment #35
marvin_b8 commentedComment #38
csheltonlcm commentedI've rerolled the patch from #35 to apply cleanly on 8.3.x. It doesn't pass testing, though.
Comment #39
csheltonlcm commentedFixed the remanining bugs in
core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.phpandcore/modules/big_pipe/src/Tests/BigPipeTest.phprelated 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
Comment #41
csheltonlcm commentedComment #42
dawehnerI'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.
Comment #43
csheltonlcm commentedYeah, 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
Comment #44
pwolanin commented@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?
__METHOD__ is the string 'Drupal\Core\Form\FormBuilder::prepareForm'
Comment #45
pwolanin commentedLike 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.
Comment #46
jhedstromThis 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.Comment #47
pwolanin commentedupdatng title and summary
Comment #48
pwolanin commentedtypo
Comment #49
jhedstromFrom #31:
I think at least some of these get cached, so an empty update hook should be added.
Comment #50
pwolanin commentedok, here it is
Comment #51
jhedstromThis 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.
Comment #52
jhedstromAssuming 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).
Comment #54
dawehnerI 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()vsCrypt::hashBase64()This is for me all about the readability, nothing whatever historic fight you had in the past.
Comment #55
wim leersThis does not improve security. It at most improves security perception, by not getting alarming results out of poor security audits.
Comment #56
alexpottI 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.
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_cachetosystem_post_update_clear_8_3I 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.
Comment #57
pwolanin commented@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
Comment #58
pwolanin commentedHere'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.
Comment #59
alexpottI 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.
Comment #60
alexpottAlso 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.
Comment #61
pwolanin commented@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.
Comment #62
pwolanin commentedAfter 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
Comment #64
pwolanin commentedFix to make the current prefix accessible from the TwigEnvironment
Comment #65
pwolanin commentedCode comment fix.
Comment #66
alexpottThis 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.
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.
Comment #67
pwolanin commented@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.
Comment #68
pwolanin commentedOk, talked to alexpott in IRC - let's just try using the settings directly in the key using json_encode().
Comment #69
fabianx commentedI 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?
Comment #70
alexpott@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.
Comment #71
pwolanin commentedAdded a draft change record at https://www.drupal.org/node/2833264 - let me know if that covers all that's needed.
Comment #72
dawehner@pwolanin
I think more important the CR should contain the information that you should not use
'crc32b'or'sha1'but ratherCrypt::hashBase64Comment #73
pwolanin commented@dawehner - ok - that was already true, but we can certainly emphasize it.
Comment #74
pwolanin commented@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.
Comment #75
fabianx commentedThanks, 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'
Comment #76
alexpottThis 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.

Comment #77
alexpottSo I think I've satisfied my performance concerns.
Committed 94f0aac and pushed to 8.3.x. Thanks!
removed unused use on commit.