Opened 4 weeks ago
Closed 4 weeks ago
#36572 closed Bug (fixed)
Deprecation of constant_time_compare broke usage with mixed-type arguments.
Reported by: | Sage Abdullah | Owned by: | Sarah Boyce |
---|---|---|---|
Component: | Utilities | Version: | dev |
Severity: | Release blocker | Keywords: | |
Cc: | Tim Graham, Jake Howard, JaeHyuckSa | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
The deprecation of constant_time_compare
in #36546 (0246f478882c26bc1fe293224653074cd46a90d0) removed the force_bytes
conversion of the arguments passed to the function. The function now raises an error if passed arguments of different types, e.g. bytes
and str
. Test:
-
tests/utils_tests/test_crypto.py
diff --git a/tests/utils_tests/test_crypto.py b/tests/utils_tests/test_crypto.py index bbedb3080d..4ed8167150 100644
a b class TestUtilsCryptoMisc(SimpleTestCase): 21 21 self.assertFalse(constant_time_compare(b"spam", b"eggs")) 22 22 self.assertTrue(constant_time_compare("spam", "spam")) 23 23 self.assertFalse(constant_time_compare("spam", "eggs")) 24 self.assertTrue(constant_time_compare(b"spam", "spam")) 25 self.assertFalse(constant_time_compare("spam", b"eggs")) 24 26 25 27 def test_constant_time_compare_deprecated(self): 26 28 msg = (
The fix on my side is trivial (ensure both arguments are the same type), but I'm not sure if this was intentional for the deprecation process. If it were intentional, I'm happy to close this as a wontfix. Otherwise, I'm also happy to send a PR that adds the force_bytes
back in.
Change History (9)
comment:1 by , 4 weeks ago
comment:2 by , 4 weeks ago
Cc: | added |
---|
comment:3 by , 4 weeks ago
Is this something you spotted and made you concerned or has this broken something on a project?
I spotted this in Wagtail (ref: https://github.com/wagtail/wagtail/pull/13363) because apparently our use of the function involves passing mixed-type arguments to the function, which likely was unintentional.
However, I can see another issue with leaving out force_bytes
: the str
vs. str
use of hmac.compare_digest()
only works with ASCII characters. This means the following test now also errors:
-
tests/utils_tests/test_crypto.py
diff --git a/tests/utils_tests/test_crypto.py b/tests/utils_tests/test_crypto.py index bbedb3080d..e1795c766e 100644
a b class TestUtilsCryptoMisc(SimpleTestCase): 21 21 self.assertFalse(constant_time_compare(b"spam", b"eggs")) 22 22 self.assertTrue(constant_time_compare("spam", "spam")) 23 23 self.assertFalse(constant_time_compare("spam", "eggs")) 24 self.assertTrue(constant_time_compare(b"spam", "spam")) 25 self.assertFalse(constant_time_compare("spam", b"eggs")) 26 self.assertTrue(constant_time_compare("ありがとう", "ありがとう")) 27 self.assertFalse(constant_time_compare("ありがとう", "おはよう")) 24 28 25 29 def test_constant_time_compare_deprecated(self): 26 30 msg = (
If developers use the utility function to e.g. compare passwords, and they just pass plain strings to the function, it no longer works if the string contains non-ASCII characters.
comment:4 by , 4 weeks ago
I hadn't noticed that the original patch changed the implementation of constant_time_compare()
(removing force_bytes()
). With this new information, that's certainly inappropriate.
I suppose there's a larger question of whether we keep the constant_time_compare()
API since we now learned it provides genuinely different functionality that third-party code is relying on (comparing strings with non-ASCII characters and arguments of different type) than hmac.compare_digest()
. In argument against keeping it, it forces third-parties to identify these areas and add their own casting rather than imposing a casting penalty where it's unneeded.
As far as I see, Django itself doesn't need to use constant_time_compare()
internally since in always compares digests. Well, hmac.compare_digest(request.session.get(HASH_SESSION_KEY, ""), session_auth_hash)
looks like the empty string needs to become a bytestring.
At this point in the release cycle, probably it's best to revert the deprecation to consider this more carefully. A later first step could be to replace internal usage of constant_time_compare()
with compare_digest()
(part of the original patch, but checked more carefully to make sure a str/bytes comparison can't happen).
comment:5 by , 4 weeks ago
Cc: | added |
---|
I agree with reverting #36546 and reviewing each case more closely. An alternative would be to explicitly state using force_bytes
in the deprecation message, but that ends up quite verbose)
Given the complexities around using compare_digest
with the correct types, I'd be in favour of keeping constant_time_compare
around to hide those from users, even if we slowly port Django's internal uses to compare_digest
separately.
comment:6 by , 4 weeks ago
Has patch: | set |
---|---|
Owner: | set to |
Severity: | Normal → Release blocker |
Status: | new → assigned |
Triage Stage: | Unreviewed → Accepted |
comment:7 by , 4 weeks ago
Cc: | added |
---|
comment:8 by , 4 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
This was mentioned in the ticket description #36546
Is this something you spotted and made you concerned or has this broken something on a project?