Require signatures on HEAD requests to peer-only endpoints#3235
Merged
Require signatures on HEAD requests to peer-only endpoints#3235
Conversation
verify_signature() short-circuits on HEAD so caches and link-checkers can probe public endpoints without needing to sign. That bypass is the right default, but it also applied to peer-only routes that pass $force_signature = true (e.g. FEP-8fcf's /followers/sync). The result: an unauthenticated HEAD on /followers/sync returned 200 — disclosing that the actor exists and the endpoint is reachable, which is exactly what FEP-8fcf's mandatory-signing requirement is meant to prevent. The response body was empty, so no follower data ever leaked, but the existence-probe still violates the contract. Tighten the bypass to only apply when $force_signature is false; mandatory-signing endpoints now require the signature on HEAD too.
There was a problem hiding this comment.
Pull request overview
This PR tightens REST signature verification behavior so that peer-only endpoints (those calling verify_signature(..., true)) no longer bypass authentication for HEAD requests, aligning HEAD with GET for mandatory-signing routes (notably FEP-8fcf /followers/sync).
Changes:
- Restricts the
HEADshort-circuit inverify_signature()to only apply when$force_signatureisfalse. - Adds a PHPUnit test covering unsigned
HEADrequests to/followers/syncexpecting a401. - Adds a security changelog entry documenting the behavior change.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| includes/rest/trait-verification.php | Updates verify_signature() so unsigned HEAD is only bypassed for non-forced endpoints. |
| tests/phpunit/tests/includes/rest/class-test-followers-controller.php | Adds test coverage for rejecting unsigned HEAD to the peer-only followers sync route. |
| .github/changelog/harden-head-mandatory-auth | Documents the security hardening in the changelog system. |
The previous assertion only checked the 401 status. Tighten it to also verify the response body's error code is `activitypub_signature_verification`, so a future refactor that returns a different 401 (e.g. authority mismatch reshaped) doesn't silently pass the test.
The test mutates activitypub_actor_mode and called delete_option() only at the end. If an assertion failed mid-test the option would leak into other tests in the suite. Move cleanup into a finally block so it always runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes:
verify_signature()short-circuits on HEAD so caches and link-checkers can probe public endpoints without needing to sign — that's the right default. But the bypass also applied to peer-only routes that pass$force_signature = true(today only FEP-8fcf's/followers/sync). Result: an unauthenticated HEAD on/followers/syncreached the handler, which then rejected with 403 (authority mismatch) — same outcome as a malformed authority, instead of the spec-correct 401 (signature required).$force_signatureis false. Mandatory-signing endpoints now reject unsigned HEAD with 401, matching how unsigned GET on the same endpoint is rejected.Browser-app compatibility verified:
OPTIONS, not HEAD. WP core'srest_handle_options_requesthandles OPTIONS via therest_pre_dispatchfilter and never reachesverify_signature.class-router.php:92is on the WordPress main-query path (outbox permalinks), not REST routes — also untouched.$force_signature = trueis/followers/sync, which is server-to-server (FEP-8fcf), not browser-facing.So this fix narrows the HEAD bypass to non-mandatory-auth endpoints while leaving every browser flow untouched.
Other information:
Testing instructions:
/followers/syncrequest with a valid keyId and round-trip it as a GET. Should respond as before.HEADwithout a Signature header. Should now return401withactivitypub_signature_verification(was403 activitypub_authority_mismatch)./inbox,/actors/{id},/oauth/authorization-server-metadata, etc.) still returns whatever it would have returned on GET, body stripped. No regression for caches/link-checkers.verify_signatureruns).Changelog entry
Changelog Entry Details
Significance
Type
Message
Required signatures on HEAD requests to peer-only endpoints.