From 5e48d4bc594e21f204b1f202558a499049f099a4 Mon Sep 17 00:00:00 2001 From: mo7ty Date: Mon, 2 Jun 2025 21:41:11 +0100 Subject: [PATCH 1/5] Tweak `passlib.handlers.bcrypt._BcryptCommon._finalize_backend_mixin` * Use declared `IDENT_*`'s in corresponding checks --- passlib/handlers/bcrypt.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index e8cf621..5d6fea6 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -43,7 +43,7 @@ _BNULL = b"\x00" # reference hash of "test", used in various self-checks -TEST_HASH_2A = "$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK" +TEST_HASH_2A = f"{IDENT_2A}04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK" def _detect_pybcrypt(): @@ -415,9 +415,9 @@ def assert_lacks_wrap_bug(ident): result = safe_verify("test", TEST_HASH_2A) if result is NotImplemented: # 2a support is required, and should always be present - raise RuntimeError(f"{backend} lacks support for $2a$ hashes") + raise RuntimeError(f"{backend} lacks support for {IDENT_2A} hashes") if not result: - raise RuntimeError(f"{backend} incorrectly rejected $2a$ hash") + raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2A} hash") assert_lacks_8bit_bug(IDENT_2A) if detect_wrap_bug(IDENT_2A): if backend == "os_crypt": @@ -425,8 +425,8 @@ def assert_lacks_wrap_bug(ident): # they'll have proper 2b implementation which will be used for new hashes. # so even if we didn't have a workaround, this bug wouldn't be a concern. logger.debug( - "%r backend has $2a$ bsd wraparound bug, enabling workaround", - backend, + "%r backend has %s bsd wraparound bug, enabling workaround", + backend, IDENT_2A ) else: # installed library has the bug -- want to let users know, @@ -443,13 +443,13 @@ def assert_lacks_wrap_bug(ident): # ---------------------------------------------------------------- # check for 2y support # ---------------------------------------------------------------- - test_hash_2y = TEST_HASH_2A.replace("2a", "2y") + test_hash_2y = TEST_HASH_2A.replace(IDENT_2A, IDENT_2Y) result = safe_verify("test", test_hash_2y) if result is NotImplemented: mixin_cls._lacks_2y_support = True - logger.debug("%r backend lacks $2y$ support, enabling workaround", backend) + logger.debug("%r backend lacks %s support, enabling workaround", backend, IDENT_2Y) elif not result: - raise RuntimeError(f"{backend} incorrectly rejected $2y$ hash") + raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2Y} hash") else: # NOTE: Not using this as fallback candidate, # lacks wide enough support across implementations. @@ -463,13 +463,13 @@ def assert_lacks_wrap_bug(ident): # ---------------------------------------------------------------- # check for 2b support # ---------------------------------------------------------------- - test_hash_2b = TEST_HASH_2A.replace("2a", "2b") + test_hash_2b = TEST_HASH_2A.replace(IDENT_2A, IDENT_2B) result = safe_verify("test", test_hash_2b) if result is NotImplemented: mixin_cls._lacks_2b_support = True - logger.debug("%r backend lacks $2b$ support, enabling workaround", backend) + logger.debug("%r backend lacks %s support, enabling workaround", backend, IDENT_2B) elif not result: - raise RuntimeError(f"{backend} incorrectly rejected $2b$ hash") + raise RuntimeError(f"{backend} incorrectly rejected {IDENT_2B} hash") else: mixin_cls._fallback_ident = IDENT_2B assert_lacks_8bit_bug(IDENT_2B) @@ -570,7 +570,7 @@ def _norm_digest_args(cls, secret, ident, new=False): elif ident == IDENT_2X: # NOTE: shouldn't get here. # XXX: could check if backend does actually offer 'support' - raise RuntimeError("$2x$ hashes not currently supported by passlib") + raise RuntimeError(f"{IDENT_2X} hashes not currently supported by passlib") else: raise AssertionError(f"unexpected ident value: {ident!r}") From 7eaf05fb748062fb6f7ea519ff1d045909066a3d Mon Sep 17 00:00:00 2001 From: mo7ty Date: Sun, 28 Sep 2025 18:30:06 +0100 Subject: [PATCH 2/5] Handle bcrypt >= 5.0.0 ValueError during _finalize_backend_mixin * Refer to [BCrypt wrap bug detected #18](https://github.com/notypecheck/passlib/issues/18) --- passlib/handlers/bcrypt.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 5d6fea6..12459d7 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -147,6 +147,7 @@ class _BcryptCommon( # type: ignore[misc] # NOTE: these are only set on the backend mixin classes _workrounds_initialized = False _has_2a_wraparound_bug = False + _fails_on_wraparound_bug = False _lacks_20_support = False _lacks_2y_support = False _lacks_2b_support = False @@ -372,7 +373,7 @@ def detect_wrap_bug(ident): ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6" ) - if verify(secret, bug_hash): + if handled_verify_wrap_error(secret, bug_hash): return True # if it doesn't have wraparound bug, make sure it *does* handle things @@ -381,13 +382,27 @@ def detect_wrap_bug(ident): ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi" ) - if not verify(secret, correct_hash): + if not handled_verify_wrap_error(mixin_cls.wrap_if_fails_on_wraparound_bug(secret), correct_hash): raise RuntimeError( f"{backend} backend failed to verify {ident} wraparound hash" ) return False + def handled_verify_wrap_error(secret, test_hash): + try: + return verify(secret, test_hash) + except ValueError as e: + if mixin_cls._fails_on_wraparound_bug: + logger.warning( + "trapped %r backend %r", + backend, + e, + exc_info=True, + ) + return False + raise e + def assert_lacks_wrap_bug(ident): if not detect_wrap_bug(ident): return @@ -577,6 +592,12 @@ def _norm_digest_args(cls, secret, ident, new=False): return secret, ident + @classmethod + def wrap_if_fails_on_wraparound_bug(cls, secret): + return (secret[:cls.truncate_size] if cls._fails_on_wraparound_bug + and len(secret) > cls.truncate_size + else secret) + class _NoBackend(_BcryptCommon): """ @@ -609,6 +630,7 @@ def _load_backend_mixin(mixin_cls, name, dryrun): return False try: version = metadata.version("bcrypt") + mixin_cls._fails_on_wraparound_bug = version >= "5.0.0" except Exception: logger.warning("(trapped) error reading bcrypt version", exc_info=True) version = "" From 775b06b11e3dd60bf1bba396b91f9c2b2c92b434 Mon Sep 17 00:00:00 2001 From: mo7ty Date: Tue, 30 Sep 2025 09:12:46 +0100 Subject: [PATCH 3/5] Update bcrypt >= 5.0.0 ValueError detection --- passlib/handlers/bcrypt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 12459d7..1a33b78 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -392,16 +392,17 @@ def detect_wrap_bug(ident): def handled_verify_wrap_error(secret, test_hash): try: return verify(secret, test_hash) - except ValueError as e: - if mixin_cls._fails_on_wraparound_bug: + except ValueError as err: + if mixin_cls._fails_on_wraparound_bug \ + and any(word in str(err) for word in ["password", str(mixin_cls.truncate_size)]): logger.warning( "trapped %r backend %r", backend, - e, + err, exc_info=True, ) return False - raise e + raise err def assert_lacks_wrap_bug(ident): if not detect_wrap_bug(ident): From 050d68e76573f0f56c5045f6077135754a9ed6fc Mon Sep 17 00:00:00 2001 From: mo7ty Date: Tue, 30 Sep 2025 11:38:12 +0100 Subject: [PATCH 4/5] Add dynamic update of `test_handlers_bcrypt._bcrypt_test` data * Wrap `known_correct_hashes` if handler `_fails_on_wraparound_bug` * This updates used data for `` --- tests/test_handlers_bcrypt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_handlers_bcrypt.py b/tests/test_handlers_bcrypt.py index b49c1fb..e0796b2 100644 --- a/tests/test_handlers_bcrypt.py +++ b/tests/test_handlers_bcrypt.py @@ -187,6 +187,11 @@ def setUp(self): self.addCleanup(os.environ.__delitem__, key) os.environ[key] = "true" + wrapped_correct_hashes = [] + for secret, hash in self.known_correct_hashes: + wrapped_correct_hashes.append((self.handler.wrap_if_fails_on_wraparound_bug(secret), hash)) + type(self).known_correct_hashes = wrapped_correct_hashes + super().setUp() # silence this warning, will come up a bunch during testing of old 2a hashes. From f01cc58757fc36f1f2c91a0b085da1f48703c70d Mon Sep 17 00:00:00 2001 From: mo7ty Date: Tue, 30 Sep 2025 12:37:54 +0100 Subject: [PATCH 5/5] Add dynamic update of `test_handlers_bcrypt._bcrypt_test` used handler `truncate_error` * Set used handler `truncate_error = _fails_on_wraparound_bug` * This updates flag for `` --- tests/test_handlers_bcrypt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_handlers_bcrypt.py b/tests/test_handlers_bcrypt.py index e0796b2..55655ad 100644 --- a/tests/test_handlers_bcrypt.py +++ b/tests/test_handlers_bcrypt.py @@ -192,6 +192,8 @@ def setUp(self): wrapped_correct_hashes.append((self.handler.wrap_if_fails_on_wraparound_bug(secret), hash)) type(self).known_correct_hashes = wrapped_correct_hashes + self.handler.truncate_error = self.handler._fails_on_wraparound_bug + super().setUp() # silence this warning, will come up a bunch during testing of old 2a hashes.