Skip to content

Decode percent-encoded authority before the followers/sync blocklist#3234

Merged
pfefferle merged 5 commits intotrunkfrom
harden/followers-sync-authority-urldecode
Apr 27, 2026
Merged

Decode percent-encoded authority before the followers/sync blocklist#3234
pfefferle merged 5 commits intotrunkfrom
harden/followers-sync-authority-urldecode

Conversation

@pfefferle
Copy link
Copy Markdown
Member

Proposed changes:

  • The /followers/sync authority validate_callback parsed the host straight out of wp_parse_url() without first decoding percent-encoded characters. A value like https://%5B::1%5D/ — bracketed IPv6 literal hidden behind %5B/%5D — yielded a host string of %5B: that didn't match any of the localhost / *.local / IP-literal exclusions, so it slipped past the validate_callback.
  • The downstream signer-host check at get_partial_followers() would still reject the request (the signer's keyId never decodes to %5b:), so this isn't an exploitable SSRF on its own. The fix lines up the validator's decisions with the real host the request would target — defense-in-depth.
  • Run urldecode() on the input before parsing so encoded forms are checked against the same blocklist as their decoded equivalents.
  • Three encoded variants added to the test data provider: %6Cocalhost, %5B::1%5D, %5B::ffff:10.0.0.1%5D.

This is opt-in defense-in-depth — the touched path only fires when FEP-8fcf /followers/sync is hit by a remote peer.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Sign a /followers/sync request with authority=https://%5B::1%5D/. The validate_callback should reject it before signature verification, returning 400 rest_invalid_param.
  • Same with authority=https://%6Cocalhost (encoded localhost) — also rejected at the validator.
  • Existing un-encoded authority strings continue to behave the same.

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch

Type

  • Security

Message

Decoded percent-encoded forms in the follower sync authority before the safety check.

Copilot AI review requested due to automatic review settings April 27, 2026 10:28
@pfefferle pfefferle self-assigned this Apr 27, 2026
@pfefferle pfefferle requested a review from a team April 27, 2026 10:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the /followers/sync REST route’s authority validation by ensuring percent-encoded hosts are decoded before applying the internal-host blocklist, aligning validation with the effective target host for defense-in-depth.

Changes:

  • Decode the authority value prior to wp_parse_url() in the /followers/sync validate_callback to catch percent-encoded internal hosts.
  • Extend the PHPUnit data provider to cover percent-encoded localhost and bracketed IPv6 literals.
  • Add a security changelog entry documenting the hardening.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
includes/rest/class-followers-controller.php Decodes authority before host parsing/blocklist check in the route validator.
tests/phpunit/tests/includes/rest/class-test-followers-controller.php Adds percent-encoded authority variants to the rejection test data provider.
.github/changelog/harden-followers-sync-authority-urldecode Adds a patch-level security changelog entry for the behavior change.

Comment thread includes/rest/class-followers-controller.php Outdated
Comment thread includes/rest/class-followers-controller.php Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread includes/rest/class-followers-controller.php
The /followers/sync authority validate_callback parsed the host
straight out of wp_parse_url() without first decoding percent-
encoded characters. A value like https://%5B::1%5D/ — bracketed
IPv6 literal hidden behind %5B/%5D — would yield a host string of
"%5B:" that didn't match any of the localhost / *.local /
IP-literal exclusions, so it slipped past the validate_callback.
The downstream signer-host check would still reject it (the
signer's keyId never decodes to %5b:), so this isn't an exploitable
SSRF on its own — but it's defense-in-depth that lines up the
validator's decisions with the real host the request would target.

Run urldecode() on the input before parsing so encoded forms are
checked against the same blocklist as their decoded equivalents.
Cover three encoded variants in the rejected_authority data
provider.
urldecode() also turns '+' into a space, which would corrupt
otherwise-valid reg-name hosts that legitimately contain '+'
(e.g. test/staging hosts like example+test.com). rawurldecode()
performs the percent-decoding we want without that querystring
'+' semantic. Also drop the trailing slash from the docblock
example so it matches the route's `^https?://[^/]+$` pattern.
The validate_callback now rawurldecodes $param before extracting the
host, but get_partial_followers() still parsed the raw authority.
That meant a percent-encoded but otherwise-valid host (e.g.
"https://%70eer.example") could pass validation yet fail the
authority-mismatch check downstream because the signer's keyId would
decode to a different string than the raw asked_host.

Apply the same rawurldecode() in get_partial_followers() so the two
sites agree on the canonical form.
@pfefferle pfefferle force-pushed the harden/followers-sync-authority-urldecode branch from 99a585d to 1dc7280 Compare April 27, 2026 11:12
@pfefferle pfefferle requested a review from Copilot April 27, 2026 11:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread includes/rest/class-followers-controller.php Outdated
The previous fix only canonicalised $authority on the signer-host
comparison. The Followers::get_by_authority() meta query and the
response `id` still received the raw, possibly percent-encoded value.
A request with `https://%70eer.example` would pass the match (both
sides decode to `peer.example`) but the LIKE meta query would search
for the literal `%70eer.example` and never hit stored inbox URLs like
`https://peer.example/inbox`, returning an empty follower set.

Decode once at the top of the handler and use that canonical string
throughout — match check, follower query, and response `id`. Adds a
regression test that seeds a follower on `peer.example`, asks with
`https://%70eer.example`, and asserts the alice follower is returned.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread includes/rest/class-followers-controller.php
Comment thread includes/rest/class-followers-controller.php
@pfefferle pfefferle merged commit 0d8fd01 into trunk Apr 27, 2026
11 checks passed
@pfefferle pfefferle deleted the harden/followers-sync-authority-urldecode branch April 27, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants