Skip to content

Normalize blocklist hosts to prevent case/trailing-dot bypass#3365

Open
pfefferle wants to merge 2 commits into
trunkfrom
fix/blocklist-host-normalization
Open

Normalize blocklist hosts to prevent case/trailing-dot bypass#3365
pfefferle wants to merge 2 commits into
trunkfrom
fix/blocklist-host-normalization

Conversation

@pfefferle

Copy link
Copy Markdown
Member

Proposed changes:

Domain blocklists compared hosts as raw strings, but DNS hosts are case-insensitive and a trailing dot (example.com.) denotes the same absolute host. So a blocked domain could be slipped past with a case variant (Example.com, EXAMPLE.COM) or a trailing dot.

This normalizes hosts on both sides of every comparison so the asymmetry is gone:

  • Added a private Moderation::normalize_host() that lower-cases, strips IPv6 brackets, and trims a trailing dot (matching the existing helper in the followers REST controller).
  • Applied it when comparing incoming actor/activity hosts in is_actor_blocked() and check_activity_against_blocks() (covers the actor, activity id, and object URI hosts).
  • Applied it when storing and removing blocked domains (add_user_block, remove_user_block, add_site_block, add_site_blocks, remove_site_block) so stored values are canonical too. Without this, a mixed-case stored domain would never match a normalized incoming host.

Other information:

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

Testing instructions:

  • Block a domain site-wide, e.g. example.com (Settings → ActivityPub → Moderation, or Moderation::add_site_block( 'domain', 'example.com' )).
  • Confirm an actor on https://Example.com/@user, https://EXAMPLE.COM/@user, and https://example.com./@user is now blocked (previously these slipped through).
  • Confirm a genuinely different domain (example.org) is still allowed.
  • Block a domain using a mixed-case/trailing-dot value (Example.COM.) and confirm it is stored as example.com and still removable.
  • Run the test suite: npm run env-test -- --filter=Moderation.

Changelog entry

Changelog fragment included in .github/changelog/fix-blocklist-host-normalization (Security / Patch).

DNS hosts are case-insensitive and a trailing dot denotes the same
absolute host, so case variants or a trailing dot could slip past a
domain block. Normalize hosts (lowercase, strip IPv6 brackets, trim
trailing dot) both when comparing incoming activity/actor hosts and
when storing/removing blocked domains, keeping both sides canonical.
Copilot AI review requested due to automatic review settings June 3, 2026 07:00
@pfefferle pfefferle self-assigned this Jun 3, 2026
@pfefferle pfefferle requested a review from a team June 3, 2026 07:00
@pfefferle pfefferle added the Bug Something isn't working label Jun 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

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 domain blocklist matching logic in Moderation by normalizing hosts so attackers can’t bypass blocks using case variants (e.g., EXAMPLE.com) or a trailing FQDN dot (e.g., example.com.).

Changes:

  • Added Moderation::normalize_host() and applied it to domain block inserts/removals so stored block values are canonical.
  • Normalized incoming hosts during block checks in is_actor_blocked() and check_activity_against_blocks() (actor/activity/object host comparisons).
  • Added PHPUnit coverage for normalization behavior and included a security/patch changelog fragment.

Reviewed changes

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

File Description
includes/class-moderation.php Normalizes domain values on write and normalizes attacker-controlled hosts during domain-block comparisons.
tests/phpunit/tests/includes/class-test-moderation.php Adds tests ensuring domain blocks can’t be bypassed via case or trailing-dot variants.
.github/changelog/fix-blocklist-host-normalization Documents the security fix in a changelog fragment.

Comment on lines +345 to 349
// Check site-wide domain blocks. Normalize the (attacker-controlled) host so a case
// variant or trailing dot cannot slip past a block; stored domains are normalized on insert.
$actor_domain = self::normalize_host( \wp_parse_url( $actor_uri, PHP_URL_HOST ) );
if ( $actor_domain && \in_array( $actor_domain, $site_blocks['domains'], true ) ) {
return true;
Comment on lines +415 to 421
// Check blocked domains. Normalize the (attacker-controlled) hosts so a case variant
// or trailing dot cannot slip past a block; stored domains are normalized on insert.
$urls = array(
\wp_parse_url( $actor_id, PHP_URL_HOST ),
\wp_parse_url( $activity->get_id(), PHP_URL_HOST ),
\wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ),
self::normalize_host( \wp_parse_url( $actor_id, PHP_URL_HOST ) ),
self::normalize_host( \wp_parse_url( $activity->get_id(), PHP_URL_HOST ) ),
self::normalize_host( \wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ) ),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants