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):  
    2121        self.assertFalse(constant_time_compare(b"spam", b"eggs"))
    2222        self.assertTrue(constant_time_compare("spam", "spam"))
    2323        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"))
    2426
    2527    def test_constant_time_compare_deprecated(self):
    2628        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 Sarah Boyce, 4 weeks ago

This was mentioned in the ticket description #36546

constant_time_compare() does call force_bytes() on its arguments but this was a workaround for Python 2.7 (7e3cf3cfd27e53ced0a1fc65a02849f78a292d3d) and no tests in Django's test suite fail with those calls removed.

Is this something you spotted and made you concerned or has this broken something on a project?

comment:2 by Sarah Boyce, 4 weeks ago

Cc: Tim Graham added

comment:3 by Sage Abdullah, 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):  
    2121        self.assertFalse(constant_time_compare(b"spam", b"eggs"))
    2222        self.assertTrue(constant_time_compare("spam", "spam"))
    2323        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("ありがとう", "おはよう"))
    2428
    2529    def test_constant_time_compare_deprecated(self):
    2630        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 Tim Graham, 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 Jake Howard, 4 weeks ago

Cc: Jake Howard 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 Sarah Boyce, 4 weeks ago

Has patch: set
Owner: set to Sarah Boyce
Severity: NormalRelease blocker
Status: newassigned
Triage Stage: UnreviewedAccepted

comment:7 by JaeHyuckSa, 4 weeks ago

Cc: JaeHyuckSa added

comment:8 by Sarah Boyce, 4 weeks ago

Triage Stage: AcceptedReady for checkin

comment:9 by Sarah Boyce <42296566+sarahboyce@…>, 4 weeks ago

Resolution: fixed
Status: assignedclosed

In d0e4dd5c:

Fixed #36572 -- Revert "Fixed #36546 -- Deprecated django.utils.crypto.constant_time_compare() in favor of hmac.compare_digest()."

This reverts commit 0246f478882c26bc1fe293224653074cd46a90d0.

Note: See TracTickets for help on using tickets.
Back to Top