test[faustwp]: regression test for hash_equals secret-key comparison (follow-up to #2312)#2362
Conversation
|
📦 Next.js Bundle Analysis for @faustwp/getting-started-exampleThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
Adds tests/integration/RestCallbacksTests.php covering the hash_equals() fix landed in #2312 (closes #2310), which previously had no direct regression coverage. canary now has the timing-safe comparison shipped but the test scaffolding was on a separate fork-branch that couldn't be pushed to; this PR brings the safety net in. Tests: - rest_authorize_permission_callback (x-faustwp-secret): match, mismatch, missing header, empty header, server-side secret unset - wpac_authorize_permission_callback (deprecated x-wpe-headless-secret): match, mismatch - Source-level guard test_secret_comparisons_use_constant_time_hash_equals asserts the known-bad '=== $header_key' and '!== $_SERVER[...]' patterns are not present and hash_equals() is. Behavioural tests cannot distinguish hash_equals() from === for valid/invalid inputs (boolean result is the same; the security difference is timing), so this source assertion is the only way to guard against a future revert of the #2310 fix. The 'secret unset' case deletes the faustwp_settings option directly rather than setting secret_key='' because the sanitize_option_faustwp_settings filter would silently restore the previous valid UUID. Verified red-on-revert against canary (manually reverted hash_equals back to ===/!==): the source guard fails cleanly. Restoring hash_equals greens the suite. 8 tests, 11 assertions. Full plugin suite green on single-site and multisite (composer test). Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
5991f12 to
5f25cae
Compare
There was a problem hiding this comment.
Pull request overview
Adds integration/regression coverage for FaustWP’s REST permission callbacks, specifically guarding against regressions of the timing-safe hash_equals() secret comparison introduced in #2312/#2310.
Changes:
- Add a new integration test suite covering
rest_authorize_permission_callback()behavior across match/mismatch/missing/empty/unset-secret scenarios. - Add behavioral coverage for the deprecated
wpac_authorize_permission_callback(). - Add a source-level regression guard intended to fail if comparisons revert away from
hash_equals().
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…uard Per Copilot review: the comment said 'three bad patterns' but only two assertStringNotContainsString calls follow. The original #2312 fix replaced ===/!== at three call sites (rest_authorize_permission_callback, wpac_authorize_permission_callback, filter_introspection), but the two REST sites share the literal '=== $header_key' shape, so a single substring check covers both. Reword to match what's actually asserted.
Summary
Follow-up to #2312 (merged as
5d73ec86), which landed thehash_equals()fix for #2310 without regression test coverage. canary now has the timing-safe secret-key comparison in production code, but no automated guard against a future revert.This PR adds
plugins/faustwp/tests/integration/RestCallbacksTests.php— tests-only, no production code changes. Both REST permission callbacks (rest_authorize_permission_callbackand the deprecatedwpac_authorize_permission_callback) previously had zero direct test coverage; this brings coverage up plus a source-level guard against the specific revert that would reintroduce the timing-attack vulnerability.Why this is a separate PR
The original #2312 had
maintainerCanModify: false, so I couldn't push the test commit onto the contributor's fork branch. I'd opened this PR (#2362) with the same security fix plus tests, but #2312 merged first — so this PR's production-code commits are now empty against canary. Resetting the branch to a single tests-only commit keeps the safety net we built without re-shipping the already-merged code.Scope
plugins/faustwp/tests/integration/RestCallbacksTests.phpTests
Test inventory
Behavioural (
rest_authorize_permission_callback):faustwp_settingsoption directly, becausesanitize_option_faustwp_settingswould otherwise restore a UUID-shaped value)Behavioural (deprecated
wpac_authorize_permission_callback):Source-level regression guard (
test_secret_comparisons_use_constant_time_hash_equals):=== $header_keyand!== $_SERVER['HTTP_X_FAUST_SECRET']are NOT present in the callbacks files.hash_equalsIS present (assertGreaterThanOrEqual(1, ...)to avoid false positives if a future contributor adds a legitimate thirdhash_equalscall).hash_equalsfrom===for valid/invalid inputs — both return the same boolean. The security difference is timing, which is impractical to assert in unit tests. The source-level check is the only way to catch a future revert of the fix[faustwp]: secret key comparisons use timing-vulnerable === instead of hash_equals() #2310 fix.Verified red-on-revert against today's canary-based branch (HEAD
5f25cae7, 2026-05-14): withhash_equalsreverted to===/!==inrest/callbacks.phpandgraphql/callbacks.php, the source guard fails cleanly (Failed asserting that ... does not contain "=== $header_key"). Restoringhash_equalsgreens the suite. Full plugin PHPUnit run on WP 6.8: 134 tests, 303 assertions on both single-site and multisite.Related
home_url()fix (AuthCallbacksTests.php)Together these three test files give
handle_generate_endpoint,process_and_replace_blocks, and the REST permission callbacks their first dedicated regression coverage. (filter_introspectionis already covered by four pre-existing tests inGraphQLCallbacksTests.php.)Co-authored-by: latenighthackathon latenighthackathon@users.noreply.github.com