Skip to content

fix: run a dummy bcrypt check before rejecting unhashable values in BcryptHasher::verify - #521

Merged
markshust merged 1 commit into
developfrom
feature/hashing-verify-dummy-timing
Oct 6, 2026
Merged

markshust merged 1 commit into
developfrom
feature/hashing-verify-dummy-timing

Conversation

@markshust

Copy link
Copy Markdown
Collaborator

Summary

  • BcryptHasher::verify() returned false straight away for values over 72 bytes or containing a NUL byte, with no hashing work. Code that calls HashManager directly could tell "rejected for length" from "wrong password" by timing.
  • BcryptHasher now builds a dummy hash at the configured cost in its constructor and runs password_verify() against it (protected verifyDummy()) before returning false. This mirrors BcryptPasswordHasher in marko/authentication (fix!: reject bcrypt passwords over 72 bytes or containing nul #467/fix!: rehash passwords on login and back authentication with marko/hashing #508) and reuses the same dummy salt and digest.
  • HashManager::verify() passes the call to the active hasher, so it gets the fix with no change of its own. Argon2Hasher accepts every input, so it has no rejection path.
  • Updated the "Bcrypt Length Limit" section of the hashing docs page.

Test plan

  • Oversize and NUL inputs return false and run the dummy verification exactly once (a subclass records verifyDummy() calls). Valid inputs never run it.
  • The dummy hash is a well-formed 60-char $2y$ hash at the configured or default cost, and crypt() accepts it. A malformed hash would make password_verify() return instantly.
  • Through HashManager::verify() with the bcrypt driver, both refused inputs run the dummy check.
  • composer ci green locally

Fixes #517

🤖 Generated with Claude Code

https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1

…cryptHasher::verify

BcryptHasher::verify() returned false immediately for values over 72 bytes
or containing a NUL byte, doing no hashing work. Code calling HashManager
directly could tell "rejected for length" from "wrong password" by timing.

verify() now checks such a value against a precomputed dummy hash at the
configured cost before returning false, mirroring BcryptPasswordHasher in
marko/authentication. HashManager::verify() delegates to the active hasher,
so it inherits the fix; Argon2Hasher refuses no input and needs no change.

Fixes #517

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyjR4cJTfro54iFZ9CWkv1
@github-actions github-actions Bot added the bug Something isn't working label Oct 6, 2026
@markshust
markshust merged commit 9e1af8b into develop Oct 6, 2026
5 checks passed
@markshust
markshust deleted the feature/hashing-verify-dummy-timing branch October 6, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BcryptHasher::verify() in marko/hashing returns early for oversize/NUL input without a dummy check

1 participant