Skip to content

Feature/harden proxy api#1

Open
crweiner wants to merge 7 commits into
developfrom
feature/harden-proxy-api
Open

Feature/harden proxy api#1
crweiner wants to merge 7 commits into
developfrom
feature/harden-proxy-api

Conversation

@crweiner
Copy link
Copy Markdown
Member

@crweiner crweiner commented Apr 10, 2026

This PR hardens the WordPress proxy endpoint used by the plugin’s “Bypass ad blockers” feature.

The goal is to make the proxy route:

  • less discoverable
  • harder to probe blindly
  • cheaper to reject when abused
  • still functional for legitimate same-site browser traffic

This is not intended to make the endpoint secret from any client that fully loads the page. It is intended to reduce route enumeration and tighten request validation so operators can protect sites more safely.

Problem

The proxy endpoint is randomized, but in the current implementation it is still relatively easy to discover and probe:

  • the randomized route is exposed in WordPress REST discovery
  • the namespace index is publicly readable
  • the route is registered with a permissive permission_callback
  • the MU speed module reduces load, but does not reject obvious junk traffic early enough

That makes the route easier to target than it needs to be, and can force site operators into blunt edge rules that may also block legitimate Plausible tracking.

Changes

src/Proxy.php

  • Hide the Plausible proxy namespace and full event route from REST discovery via rest_route_data
  • Return 404 for namespace probing like /wp-json/<namespace>/v1/ via rest_pre_dispatch
  • Replace permission_callback => __return_true with request validation
  • Validate:
    • request body size
    • Content-Type: application/json
    • valid JSON payload
    • same-site Origin or Referer
    • allowed payload keys only
    • expected Plausible domain in d
    • local-site event URL in u
    • p must be an array if present
  • Preserve validation response codes instead of accidentally normalizing them via the response-status shim

mu-plugin/plausible-proxy-speed-module.php

  • Tighten proxy request detection to the /wp-json/<namespace> prefix
  • Short-circuit obvious junk requests before more of WordPress loads
  • Reject:
    • namespace probing
    • wrong path under the namespace
    • non-POST requests
    • non-JSON requests
    • missing same-site provenance
    • invalid payloads
    • oversized bodies
  • Use a capped buffered read of php://input for body-size enforcement instead of trusting Content-Length
  • Align host validation with src/Proxy.php, including relative URLs like /some-page

Why these changes

1. Route discovery

The proxy route is randomized, but if it appears in /wp-json/, it is still enumerable. Hiding it from REST discovery removes the most obvious public discovery surface.

2. Namespace probing

Even if the root REST index is hidden, a known namespace should not expose a readable namespace index. Returning 404 makes the route less self-documenting.

3. Request validation

The proxy endpoint is expected to receive anonymous browser traffic, but it does not need to accept arbitrary malformed traffic. Tightening validation helps reject obvious junk early.

4. Early rejection in the MU layer

The MU speed module is the cheapest place to reject bad proxy traffic. The more validation that can happen there, the less unnecessary WordPress work happens under load.

Verified behaviors

With proxy enabled:

  • /wp-json/ does not list the generated Plausible namespace/route
  • GET /wp-json/<namespace>/v1/ returns 404
  • the frontend still embeds the exact endpoint in plausible.init({ endpoint: ... })
  • valid same-site JSON POST to the full endpoint returns 202
  • valid same-site JSON POST with a relative URL like "u": "/test" also returns 202
  • missing Origin / Referer returns 404
  • foreign Origin / Referer returns 404
  • wrong domain in d returns 400
  • malformed JSON returns 400
  • disallowed payload keys return 400
  • p as a non-array returns 400
  • wrong path under the namespace returns 404
  • oversized valid JSON body returns 413

Additional behavior checks

  • With proxy disabled:
    • the local proxy endpoint stops working
    • the frontend stops overriding the endpoint
  • Without the MU plugin installed:
    • the route still works
    • route hiding still works
    • the plugin just loses the earliest short-circuit layer
  • Proxy resources can still be rotated by clearing/regenerating the stored proxy resources

Notes

  • OPTIONS / preflight requests now return 404. This is acceptable for the intended same-origin browser use case, but worth being aware of if cross-origin proxy usage is ever expected.
  • This PR does not attempt to hide the endpoint from clients that actually load the page. The endpoint still has to be present in frontend configuration for the browser to use it.
  • The purpose of this PR is to remove REST-level discoverability and tighten server-side acceptance rules, not to provide perfect secrecy.

Resolves plausible#294

Summary by CodeRabbit

  • New Features

    • Enforces a maximum request size for the proxy endpoint.
    • Hides proxy routes from REST API discovery when the proxy is enabled.
    • Namespace probing now consistently returns a 404-style response.
  • Bug Fixes

    • Blocks non-POST or non-JSON requests and oversized payloads to the proxy.
    • Adds stricter payload validation (fields, lengths, URLs) and stronger origin/referer provenance checks.

@crweiner
Copy link
Copy Markdown
Member Author

@copilot, please review this PR

… in Proxy.php and URL length check in MU plugin

Agent-Logs-Url: https://github.com/crweiner/plausible-wordpress-plugin/sessions/b35d7e5f-5764-4ad1-9b52-a62192c6ac6b

Co-authored-by: crweiner <23106097+crweiner@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented Apr 10, 2026

@copilot, please review this PR

I reviewed the PR and found two inconsistencies between the Proxy.php and MU plugin validation layers. Fixed in 06555ad:

  1. url_matches_home_host() in Proxy.php was missing an explicit if (!$host) return false; guard for non-parseable, non-relative URLs. The MU plugin had this correctly. While safe in practice (empty string won't match a real domain), the implicit path through normalize_domain(null) was unnecessarily fragile.

  2. MU plugin's has_valid_payload() was missing the strlen($data['u']) > 2048 URL length check that exists in Proxy.php. Added for consistency.

Beyond those two fixes, the rest of the implementation looks solid:

  • Route discovery hiding via rest_route_data and namespace index blocking via rest_pre_dispatch are clean
  • The force_http_response_code null-safety improvement correctly prevents status code clobbering when validation errors return WP_Error
  • The MU module's capped file_get_contents('php://input', ..., MAX_REQUEST_BYTES + 1) is a good approach for body-size enforcement
  • The defense-in-depth of duplicating validation across both layers is appropriate since the MU plugin may not always be installed
  • The plausible_analytics_proxy_require_same_origin and plausible_analytics_proxy_max_body_bytes filters provide useful escape hatches

One pre-existing note (not introduced by this PR): do_request() on line 112 constructs a path with "/$this->namespace/v1/$this->base/$this->endpoint" but $this->namespace already includes /v1, producing a doubled /v1/v1/ segment. This doesn't cause issues since do_request() calls send_event() directly and the path is never routed, but it's worth cleaning up separately.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/d95a9d895352c30f47604ad1b825ab8fa9d1a373
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/41aaac462fbd80feb8dd129e489f4bbc53fe26b0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/Yoast/wp-test-utils/zipball/2e0f62e0281e4859707c5f13b7da1422aa1c8f7b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/antecedent/patchwork/zipball/8b6b235f405af175259c8f56aea5fc23ab9f03ce
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/coenjacobs/mozart/zipball/dbcdeb992d20d9c8914eef090f9a0d684bb1102c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/composer/installers/zipball/12fb2dfe5e16183de69e784a7b84046c43d97e8e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-parallel-lint/PHP-Console-Color/zipball/7adfefd530aa2d7570ba87100a99e2483a543b88
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-parallel-lint/PHP-Console-Highlighter/zipball/5b4803384d3303cf8e84141039ef56c8a123138d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/symfony/string/zipball/50590a057841fa6bf69d12eceffce3465b9e32cb
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/thephpleague/flysystem/zipball/3239285c825c152bcc315fe0e87d6b55f5972ed1
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)
  • https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/GwJoOV /usr/bin/composer install --no-interaction (http block)

If you need me to access, download, or install something from one of these locations, you can either:

gin0115 added 2 commits April 15, 2026 14:03
…es return a 404 so you cant assume an endpoint exists based on response
Added hardening to the host matching and also to ensure that all rout…
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Walkthrough

Changes modify mu-plugin/plausible-proxy-speed-module.php and src/Proxy.php to harden and conceal the proxy REST endpoint. A new MAX_REQUEST_BYTES (8192) is added. Request-path detection now uses parsed path prefix checks. Added request short-circuiting that returns a uniform 404-style rest_no_route for namespace-index probes, non-POST methods, non-application/json content types, oversized bodies, provenance failures (Origin/Referer host vs site home), and malformed or invalid JSON payloads. New helper methods validate headers, provenance, payload fields (n, d, u, optional p/revenue), and hide proxy routes from REST discovery.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Feature/harden proxy api' is directly related to the main changes, which comprehensively harden the proxy REST API endpoint through validation, discovery-hiding, and early short-circuiting.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
mu-plugin/plausible-proxy-speed-module.php (2)

107-135: Short-circuit sequence relies on a non-obvious exit inside the helper.

Each check calls send_rest_no_route() which only terminates because send_json_error() ends in exit. If that helper is ever refactored to return or throw, the subsequent checks below would continue executing and a subsequent header()/echo would trip "headers already sent" / double output. Returning after the call (or converting to elseif) makes the contract explicit and robust.

♻️ Proposed fix
-		if ( $this->is_namespace_index_request() || ! $this->is_exact_proxy_endpoint_request() ) {
-			$this->send_rest_no_route();
-		}
-
-		if ( $this->get_request_method() !== 'POST' ) {
-			$this->send_rest_no_route();
-		}
-
-		if ( ! $this->has_json_content_type() ) {
-			$this->send_rest_no_route();
-		}
-
-		if ( ! $this->has_valid_provenance() ) {
-			$this->send_rest_no_route();
-		}
-
-		if ( $this->request_body_too_large() ) {
-			$this->send_rest_no_route();
-		}
-
-		if ( ! $this->has_valid_payload() ) {
-			$this->send_rest_no_route();
-		}
+		if (
+			$this->is_namespace_index_request()
+			|| ! $this->is_exact_proxy_endpoint_request()
+			|| $this->get_request_method() !== 'POST'
+			|| ! $this->has_json_content_type()
+			|| ! $this->has_valid_provenance()
+			|| $this->request_body_too_large()
+			|| ! $this->has_valid_payload()
+		) {
+			$this->send_rest_no_route();
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` around lines 107 - 135, The
maybe_short_circuit_request() function relies on send_rest_no_route() to
terminate execution implicitly (send_json_error() currently calls exit); make
this explicit and robust by returning immediately after each
send_rest_no_route() call (or restructure the checks into an if/elseif chain) so
subsequent checks won't run if a short-circuit response was sent; update
maybe_short_circuit_request() to call send_rest_no_route() followed by return
and keep references to send_rest_no_route() and send_json_error() for clarity.

82-90: Prefix check lacks a boundary — potential over-match.

strpos($path, '/wp-json/' . $namespace) === 0 will also match siblings like /wp-json/{namespace}something/... if some future route happens to share the prefix. With the randomized namespace this is extremely unlikely to collide in practice, but it's cheap to make the check exact by requiring the next character to be / or end-of-string.

♻️ Proposed tightening
-		return strpos( $this->get_request_path(), '/wp-json/' . $namespace ) === 0;
+		$prefix = '/wp-json/' . $namespace;
+		$path   = $this->get_request_path();
+
+		return $path === $prefix || strpos( $path, $prefix . '/' ) === 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` around lines 82 - 90, The prefix
check in is_proxy_request currently allows over-matching; update
is_proxy_request to compute $prefix = '/wp-json/'.$namespace, verify the request
path starts with that prefix (using strpos or strncmp), then ensure the
character immediately after the prefix is either a slash or there is no
character (path length equals prefix length). Use get_request_path() and
resources['namespace'] to locate values; return false if the prefix doesn't
match or the next character is neither '/' nor end-of-string, otherwise return
true.
src/Proxy.php (1)

398-427: Consider validating revenue shape.

revenue is whitelisted but otherwise passes through to send_event() unchecked. Plausible expects { currency: string, amount: number|string } here — if you care about defense-in-depth against a misbehaving tracker or an attacker forging a same-origin request, a light shape check (array with currency string and numeric amount) would keep the forwarded body tight. Not blocking if you'd rather stay permissive and let Plausible reject upstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Proxy.php` around lines 398 - 427, The payload validator currently allows
a `revenue` field through unchecked; update has_valid_payload to also validate
`revenue` (before returning true) when set: ensure isset($params['revenue'])
implies is_array($params['revenue']) with keys 'currency' and 'amount', that
'currency' is a non-empty string and 'amount' is a numeric value (or numeric
string) — reject otherwise; keep this check inside has_valid_payload so only
well-shaped revenue objects are forwarded to send_event.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mu-plugin/plausible-proxy-speed-module.php`:
- Around line 149-151: The get_request_path() helper currently only returns the
path from $this->request_uri which misses REST requests sent via query string
(e.g. ?rest_route=/namespace/v1/...) or when rest_url_prefix is customized;
update get_request_path() to first check for a $_GET['rest_route'] value and, if
present, return that route's path (normalized/trimmed of leading slash),
otherwise fall back to wp_parse_url($this->request_uri, PHP_URL_PATH) ?: ''. Use
rest_get_url_prefix() if you need to normalize or detect prefixed routes so
query-string REST requests are treated the same as pretty-permalink ones.
- Around line 305-315: The current checks call normalize_domain() on $data['d']
and other string functions without guarding types, which can cause TypeError on
non-strings; update the validation in plausible-proxy-speed-module.php to mirror
src/Proxy.php by requiring is_string() for 'n', 'd', and 'u' before any string
operations: for 'n' require is_string($data['n']) and $data['n'] !== '' and
strlen(...) <= 120; for 'd' require is_string($data['d']) before calling
normalize_domain($data['d']) and comparing to
normalize_domain($this->get_expected_domain()); for 'u' require
is_string($data['u']) before strlen(...) and before calling
$this->url_matches_home_host($data['u']); keep the same length limits and
url_matches_home_host/normalize_domain/get_expected_domain checks otherwise so
both layers accept/reject identical inputs.
- Around line 340-348: get_expected_domain() currently returns home_url()
(including scheme/path) and skips the plausible_analytics_settings filter,
causing mismatch with Helpers::get_domain() which normalizes to a bare domain;
update get_expected_domain() to obtain settings via
get_option('plausible_analytics_settings', []) (respecting existing
filter/defaults) and then either call Helpers::get_domain() to normalize the
domain or replicate its normalization (preg_replace to strip ^https?://(www\.)?)
so the function returns a bare domain like example.com matching
Helpers::get_domain() and avoiding validation divergence.
- Around line 197-214: The apply_filters call for
'plausible_analytics_proxy_require_same_origin' in has_valid_provenance is
effectively unused because no callback is registered; either register a default
filter callback during plugin initialization or remove the filter. Fix by adding
a single, documented add_filter registration in your MU-plugin bootstrap (or
plugin init hook) that returns the desired boolean (e.g., respects a defined
constant or option) so apply_filters in has_valid_provenance() and the
corresponding call in src/Proxy.php are honorably overridable, or alternatively
remove the filter usage from has_valid_provenance() and src/Proxy.php if you
decide not to support overriding.

In `@src/Proxy.php`:
- Around line 489-496: The current check uses empty( $data['response']['code'] )
which can trigger PHP 8 warnings if $data['response'] is a scalar; update the
guard to ensure $data['response'] is an array before accessing ['code'].
Specifically, in the block after $data = $response->get_data() change the
conditional to verify is_array($data) && isset($data['response']) &&
is_array($data['response']) && ! empty($data['response']['code']) (or the
logical inverse to return early), so wp_remote_retrieve_response_code($data) and
$response->set_status(...) only run when $data['response']['code'] is safely
accessible.

---

Nitpick comments:
In `@mu-plugin/plausible-proxy-speed-module.php`:
- Around line 107-135: The maybe_short_circuit_request() function relies on
send_rest_no_route() to terminate execution implicitly (send_json_error()
currently calls exit); make this explicit and robust by returning immediately
after each send_rest_no_route() call (or restructure the checks into an
if/elseif chain) so subsequent checks won't run if a short-circuit response was
sent; update maybe_short_circuit_request() to call send_rest_no_route() followed
by return and keep references to send_rest_no_route() and send_json_error() for
clarity.
- Around line 82-90: The prefix check in is_proxy_request currently allows
over-matching; update is_proxy_request to compute $prefix =
'/wp-json/'.$namespace, verify the request path starts with that prefix (using
strpos or strncmp), then ensure the character immediately after the prefix is
either a slash or there is no character (path length equals prefix length). Use
get_request_path() and resources['namespace'] to locate values; return false if
the prefix doesn't match or the next character is neither '/' nor end-of-string,
otherwise return true.

In `@src/Proxy.php`:
- Around line 398-427: The payload validator currently allows a `revenue` field
through unchecked; update has_valid_payload to also validate `revenue` (before
returning true) when set: ensure isset($params['revenue']) implies
is_array($params['revenue']) with keys 'currency' and 'amount', that 'currency'
is a non-empty string and 'amount' is a numeric value (or numeric string) —
reject otherwise; keep this check inside has_valid_payload so only well-shaped
revenue objects are forwarded to send_event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff7257bc-c655-482b-926a-3af00dd25f60

📥 Commits

Reviewing files that changed from the base of the PR and between 558eddb and 9786201.

📒 Files selected for processing (2)
  • mu-plugin/plausible-proxy-speed-module.php
  • src/Proxy.php

Comment on lines +149 to +151
private function get_request_path() {
return wp_parse_url( $this->request_uri, PHP_URL_PATH ) ?: '';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Query-string REST routing bypasses MU short-circuit.

When pretty permalinks are disabled, WordPress accepts REST requests via /?rest_route=/{namespace}/v1/.... The path-only check here sees / and treats the request as non-proxy, so all the hardened short-circuit logic is skipped (request still hits src/Proxy.php::validate_proxy_request(), so this is not a security regression — just lost early-rejection / speed benefit). Similar note for sites that customize rest_url_prefix away from wp-json. Either document the pretty-permalinks assumption or also inspect $_GET['rest_route'].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` around lines 149 - 151, The
get_request_path() helper currently only returns the path from
$this->request_uri which misses REST requests sent via query string (e.g.
?rest_route=/namespace/v1/...) or when rest_url_prefix is customized; update
get_request_path() to first check for a $_GET['rest_route'] value and, if
present, return that route's path (normalized/trimmed of leading slash),
otherwise fall back to wp_parse_url($this->request_uri, PHP_URL_PATH) ?: ''. Use
rest_get_url_prefix() if you need to normalize or detect prefixed routes so
query-string REST requests are treated the same as pretty-permalink ones.

Comment on lines +197 to +214
private function has_valid_provenance() {
if ( ! apply_filters( 'plausible_analytics_proxy_require_same_origin', true ) ) {
return true;
}

$origin = $_SERVER[ 'HTTP_ORIGIN' ] ?? '';
$referer = $_SERVER[ 'HTTP_REFERER' ] ?? '';

if ( $origin && $this->host_matches_home( $origin ) ) {
return true;
}

if ( $referer && $this->host_matches_home( $referer ) ) {
return true;
}

return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=php -C2 "plausible_analytics_proxy_require_same_origin"

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 737


🏁 Script executed:

#!/bin/bash
# Find the constructor and where has_valid_provenance is called in the MU plugin
rg -n "function __construct|has_valid_provenance" mu-plugin/plausible-proxy-speed-module.php -A 5

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 719


🏁 Script executed:

#!/bin/bash
# Check the full context of the MU plugin to understand initialization
head -50 mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 970


🏁 Script executed:

#!/bin/bash
# Search for where the MU plugin class is instantiated
rg -n "new.*plausible" mu-plugin/plausible-proxy-speed-module.php -i

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Check the init() method
sed -n '57,130p' mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 1650


🏁 Script executed:

#!/bin/bash
# Check context around instantiation at line 398
tail -20 mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 493


🏁 Script executed:

#!/bin/bash
# Check if there are any hooks where the class is registered
rg -n "add_action|do_action|add_filter" mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 159


🏁 Script executed:

#!/bin/bash
# Find send_rest_no_route method
rg -n "send_rest_no_route" mu-plugin/plausible-proxy-speed-module.php -A 5

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 901


🏁 Script executed:

#!/bin/bash
# Check WordPress mu-plugin loading documentation info - when are filters available?
# Let's also verify if there's any filter registration happening before instantiation
rg -n "^[^/]*apply_filters|^[^/]*add_filter" mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Let's verify the WordPress hook firing order - when init happens vs when plugins load
# Check if there are any comments about timing or hook order
rg -n "init|load|wp_" mu-plugin/plausible-proxy-speed-module.php | head -20

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Check if the regular plugin (src/Proxy.php) registers any callbacks before its filter use
rg -B20 "apply_filters.*plausible_analytics_proxy_require_same_origin" src/Proxy.php | head -40

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 655


🏁 Script executed:

#!/bin/bash
# Let's see the full context around the Proxy.php usage of the same filter
sed -n '340,360p' src/Proxy.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Check when exactly has_valid_provenance is called - need to understand is_proxy_request initialization
sed -n '49,105p' mu-plugin/plausible-proxy-speed-module.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 1308


🏁 Script executed:

#!/bin/bash
# Let's check if there's any way filters could be registered before the MU plugin runs
# Look for wp-config or other early loading mechanisms
git ls-files | grep -E "(wp-config|config|setup)" | head -10

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Verify when MU plugins are loaded relative to regular plugins and when filters can be registered
# Check if there's any documentation in the plugin about this
rg -n "mu.plugin|must.use|load|filter.*register" . -i --type=php --type=md | grep -i "mu\|must\|load\|early" | head -10

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 1118


🏁 Script executed:

#!/bin/bash
# Let's check when the main Plausible Analytics plugin is loaded and when it might register filters
rg -n "register_activation_hook|plugins_loaded|init" src/Plugin.php 2>/dev/null | head -20

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 396


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation about the architecture
find . -name "README*" -o -name "*.md" | head -5 | xargs cat 2>/dev/null | head -50

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 2728


🏁 Script executed:

#!/bin/bash
# Check if the filter is ever registered with add_filter anywhere in the codebase
rg -n "add_filter.*plausible_analytics_proxy_require_same_origin" .

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Also search for any function that registers this filter
rg -n "plausible_analytics_proxy_require_same_origin" . --type=php | grep -v "apply_filters"

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Let's trace where Proxy.php is instantiated and when it runs
rg -n "new Proxy|Proxy::" src/ --type=php | head -20

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 804


🏁 Script executed:

#!/bin/bash
# Check if Proxy.php is loaded through the main plugin flow
rg -n "class Proxy\|function.*proxy" src/Plugin.php -A 3 | head -30

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Let's understand: when is Proxy.php actually used? On every request or only sometimes?
rg -B5 -A5 "has_valid_provenance" src/Proxy.php | head -50

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 624


Filter has no registered callbacks and is effectively unused.

apply_filters( 'plausible_analytics_proxy_require_same_origin', true ) is applied on every proxy request in both the MU layer and src/Proxy.php, but the filter is never registered with a callback anywhere in the codebase. This means the default value of true is always used, making the filter dead code. If this filter is intended for users to override, either register default callbacks in the plugin initialization or document the requirement to hook it from wp-config.php or another MU plugin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` around lines 197 - 214, The
apply_filters call for 'plausible_analytics_proxy_require_same_origin' in
has_valid_provenance is effectively unused because no callback is registered;
either register a default filter callback during plugin initialization or remove
the filter. Fix by adding a single, documented add_filter registration in your
MU-plugin bootstrap (or plugin init hook) that returns the desired boolean
(e.g., respects a defined constant or option) so apply_filters in
has_valid_provenance() and the corresponding call in src/Proxy.php are honorably
overridable, or alternatively remove the filter usage from
has_valid_provenance() and src/Proxy.php if you decide not to support
overriding.

Comment thread mu-plugin/plausible-proxy-speed-module.php Outdated
Comment thread mu-plugin/plausible-proxy-speed-module.php
Comment thread src/Proxy.php
Comment on lines +489 to 496
$data = $response->get_data();

if ( ! is_array( $data ) || empty( $data['response']['code'] ) ) {
return $response;
}

$response_code = wp_remote_retrieve_response_code( $data );
$response->set_status( $response_code );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten the response subarray check to avoid PHP 8 warnings.

empty( $data['response']['code'] ) will emit a "cannot access offset on string" warning on PHP 8+ if $data['response'] happens to be a scalar rather than an array. Adding an is_array() guard on the intermediate offset keeps this fully warning-free and matches the intent of the existing is_array( $data ) check.

🛡️ Proposed fix
-		if ( ! is_array( $data ) || empty( $data['response']['code'] ) ) {
+		if ( ! is_array( $data ) || ! isset( $data['response'] ) || ! is_array( $data['response'] ) || empty( $data['response']['code'] ) ) {
 			return $response;
 		}
PHP 8 warning for array access with non-numeric string key on a scalar value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Proxy.php` around lines 489 - 496, The current check uses empty(
$data['response']['code'] ) which can trigger PHP 8 warnings if
$data['response'] is a scalar; update the guard to ensure $data['response'] is
an array before accessing ['code']. Specifically, in the block after $data =
$response->get_data() change the conditional to verify is_array($data) &&
isset($data['response']) && is_array($data['response']) && !
empty($data['response']['code']) (or the logical inverse to return early), so
wp_remote_retrieve_response_code($data) and $response->set_status(...) only run
when $data['response']['code'] is safely accessible.

- Guard n/d/u with isset + is_string before string ops to avoid
  TypeError on non-string payloads (which would surface as 500s
  and defeat the uniform-404 design).
- Use === '' instead of empty() for n and u so legitimate values
  like 0 aren't rejected.
- Strip scheme and www. from the home_url() fallback in
  get_expected_domain() so it returns a bare domain matching
  Helpers::get_domain(), preventing silent rejection of valid
  payloads on sites without a configured domain_name setting.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
mu-plugin/plausible-proxy-speed-module.php (1)

89-90: ⚠️ Potential issue | 🟡 Minor

Handle query-string REST routing and custom REST prefixes in path detection.

This still only recognizes pretty-permalink paths under /wp-json/.... Requests like /?rest_route=/{namespace}/v1/... (and installations with custom rest_url_prefix) bypass MU short-circuiting, so hardening/speed benefits are skipped for those requests.

💡 Proposed fix
+	private function get_rest_prefix() {
+		return '/' . trim( rest_get_url_prefix(), '/' );
+	}
+
 	private function get_request_path() {
-		return wp_parse_url( $this->request_uri, PHP_URL_PATH ) ?: '';
+		$rest_route = isset( $_GET['rest_route'] ) ? (string) wp_unslash( $_GET['rest_route'] ) : '';
+
+		if ( $rest_route !== '' ) {
+			$path = wp_parse_url( $rest_route, PHP_URL_PATH ) ?: $rest_route;
+			return '/' . ltrim( $path, '/' );
+		}
+
+		return wp_parse_url( $this->request_uri, PHP_URL_PATH ) ?: '';
 	}
 
 	private function is_proxy_request() {
 		$namespace = $this->resources[ 'namespace' ] ?? '';
@@
-		return strpos( $this->get_request_path(), '/wp-json/' . $namespace ) === 0;
+		return strpos( $this->get_request_path(), $this->get_rest_prefix() . '/' . $namespace ) === 0;
 	}
@@
-		return '/wp-json/' . $namespace . '/v1/' . $base . '/' . $endpoint;
+		return $this->get_rest_prefix() . '/' . $namespace . '/v1/' . $base . '/' . $endpoint;
 	}
@@
-		return $this->get_request_path() === '/wp-json/' . ( $this->resources[ 'namespace' ] ?? '' ) . '/v1';
+		return $this->get_request_path() === $this->get_rest_prefix() . '/' . ( $this->resources[ 'namespace' ] ?? '' ) . '/v1';
 	}
#!/bin/bash
# Verify current routing detection only uses REQUEST_URI path + hardcoded /wp-json prefix.
rg -n --type=php -C3 "get_request_path|rest_route|rest_get_url_prefix|/wp-json/" mu-plugin/plausible-proxy-speed-module.php

Also applies to: 149-151, 161-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` around lines 89 - 90, The current
path-detection only checks get_request_path() for a hardcoded
'/wp-json/{namespace}' prefix (e.g. the strpos call) and therefore misses REST
requests sent via the rest_route query string or sites with a custom
rest_url_prefix; update the detection logic used in the methods that currently
reference get_request_path()/'/wp-json/' (including the other similar checks
around the same module like the code at the other occurrences) to also inspect
$_GET['rest_route'] (normalize and trim leading slashes) and compare against
rest_get_url_prefix() .'/'. $namespace (or build the expected prefix by joining
rest_get_url_prefix() and $namespace) so both pretty permalink and query-string
REST routes (and custom rest URL prefixes) are recognized and short-circuited.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mu-plugin/plausible-proxy-speed-module.php`:
- Line 14: The MU plugin hardcodes MAX_REQUEST_BYTES (used in the constant,
request_body_too_large(), and get_request_body()) to 8192 which bypasses the
filter used in src/Proxy.php; change those sites to obtain the limit via the
same filter (plausible_analytics_proxy_max_body_bytes) instead of the hardcoded
constant so the MU layer respects configured values—replace direct uses of
MAX_REQUEST_BYTES with a call to the filter (defaulting to 8192) and ensure
request_body_too_large() and get_request_body() both use that filtered value.

---

Duplicate comments:
In `@mu-plugin/plausible-proxy-speed-module.php`:
- Around line 89-90: The current path-detection only checks get_request_path()
for a hardcoded '/wp-json/{namespace}' prefix (e.g. the strpos call) and
therefore misses REST requests sent via the rest_route query string or sites
with a custom rest_url_prefix; update the detection logic used in the methods
that currently reference get_request_path()/'/wp-json/' (including the other
similar checks around the same module like the code at the other occurrences) to
also inspect $_GET['rest_route'] (normalize and trim leading slashes) and
compare against rest_get_url_prefix() .'/'. $namespace (or build the expected
prefix by joining rest_get_url_prefix() and $namespace) so both pretty permalink
and query-string REST routes (and custom rest URL prefixes) are recognized and
short-circuited.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 748024a8-50f4-407e-9185-57ff5e2a6458

📥 Commits

Reviewing files that changed from the base of the PR and between 9786201 and 10de90c.

📒 Files selected for processing (2)
  • CHANGES.md
  • mu-plugin/plausible-proxy-speed-module.php

*/

class PlausibleProxySpeed {
const MAX_REQUEST_BYTES = 8192;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify limit handling differs between MU layer and src/Proxy.php.
rg -n --type=php -C3 "MAX_REQUEST_BYTES|plausible_analytics_proxy_max_body_bytes|request_body_too_large|get_request_body" mu-plugin/plausible-proxy-speed-module.php src/Proxy.php

Repository: a8cteam51/plausible-wordpress-plugin

Length of output: 3444


Align MU plugin's request size limit with the filterable limit in src/Proxy.php.

The MU layer hardcodes MAX_REQUEST_BYTES to 8192 in three places:

  • Line 14: constant definition
  • Line 283-284: request_body_too_large() method
  • Line 329-331: get_request_body() method parameter

Meanwhile, src/Proxy.php applies the filter plausible_analytics_proxy_max_body_bytes (line 284), allowing customization. When this filter is modified, the MU plugin still rejects requests at 8192, silently bypassing the configured limit and causing valid requests to fail.

Update the MU plugin to use the same filterable approach as src/Proxy.php, ensuring consistent behavior across both validation layers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mu-plugin/plausible-proxy-speed-module.php` at line 14, The MU plugin
hardcodes MAX_REQUEST_BYTES (used in the constant, request_body_too_large(), and
get_request_body()) to 8192 which bypasses the filter used in src/Proxy.php;
change those sites to obtain the limit via the same filter
(plausible_analytics_proxy_max_body_bytes) instead of the hardcoded constant so
the MU layer respects configured values—replace direct uses of MAX_REQUEST_BYTES
with a call to the filter (defaulting to 8192) and ensure
request_body_too_large() and get_request_body() both use that filtered value.

@crweiner
Copy link
Copy Markdown
Member Author

No need to merge this PR into our own develop branch of this fork.

I've raised a PR with the Plausible repo: plausible#300

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Obscured Plausible proxy endpoint is discoverable via /wp-json/

3 participants