[13.x] Use a timing-safe comparison for the maintenance mode bypass secret#60896
[13.x] Use a timing-safe comparison for the maintenance mode bypass secret#60896yazansalhi wants to merge 2 commits into
Conversation
The bypass secret was compared to the request path with a plain string comparison, while the bypass cookie it issues is already validated with hash_equals() in MaintenanceModeBypassCookie::isValid(). Use hash_equals() here as well so both halves of the same bypass flow are consistent. The isset() guard becomes is_string() so that a non-string secret keeps returning false rather than raising a TypeError from hash_equals().
d6d8dc8 to
809f59b
Compare
|
I agree this isn't an exploitable vulnerability, but not for the reason you give. Entropy defends against blind guessing, not against a timing oracle: a byte-at-a-time oracle converts an exponential search into a linear one, so it sidesteps entropy entirely. Worth noting the change is incomplete, too: |
The prerendered maintenance file compares the bypass secret to the request URI with a plain string comparison eleven lines above the hash_equals() call that validates the bypass cookie. This runs before the framework boots, so it needs the same treatment as the middleware. The isset() guard is kept here rather than is_string(), because the secret is concatenated onto '/' before the comparison, so a non-string secret is already coerced and behavior is unchanged for every input.
|
Thanks — you're right, the entropy reasoning was wrong; a byte-at-a-time oracle would sidestep entropy entirely. I've corrected the description. Good catch on the stub — pushed a commit fixing that one too. |
PreventRequestsDuringMaintenancecompares the bypass secret to the request path with a plain string comparison:The cookie that this branch issues is already validated with
hash_equals()inMaintenanceModeBypassCookie::isValid(), so the two halves of the same bypass flow currently use different comparison styles. This makes them consistent.The same comparison exists in
stubs/maintenance-mode.stub, eleven lines above thehash_equals()call that validates the bypass cookie, so this changes both (thanks @GrahamCampbell for pointing that one out). That path runs before the framework boots.To be clear about the scope: I am not reporting an exploitable vulnerability. No usable timing oracle exists here —
===compares lengths before content, so wrong-length guesses never reachmemcmp, and a short secret is a single vectorized compare with no per-byte gradient to climb. This is a defense-in-depth/consistency change in the same spirit as #21320, where theremember_mecomparison was moved tohash_equals().There is no behavior change in either file, for any input:
isset()guard becomesis_string(), because the original===is strict and would never have matched a non-string secret. Without the guard,hash_equals()would raise aTypeErrorwhere the old code returnedfalse.isset()guard is kept, because there the secret is concatenated onto'/'before the comparison, so a non-string secret is already coerced to a string and continues to match exactly as before.$data['secret']'foo''foo'truetrue'foo''bar'falsefalse'foo'falsefalse123(middleware)'123'falsefalse123(stub)'/123'truetrueThe existing maintenance mode tests in
tests/Integration/Foundation/MaintenanceModeTest.phpcontinue to describe the expected behavior.