Skip to content

fix: forward the visitor's own request signature on server-side detections - #47

Merged
cport1 merged 1 commit into
mainfrom
fix/forward-visitor-client-signals
Jul 26, 2026
Merged

fix: forward the visitor's own request signature on server-side detections#47
cport1 merged 1 commit into
mainfrom
fix/forward-visitor-client-signals

Conversation

@cport1

@cport1 cport1 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Restores actor correlation for WordPress sites, which a platform-side fix disabled.

What broke

The plugin beacons detections to the ingest service over its own HTTP connection, so the request arriving there carries this site's outbound headers, not the visitor's. The service was composing the visitor's network identity from those — byte-identical on every beacon a site sends — so every visitor a site ever reported was folded into one shared "actor".

The platform side shipped first (WebDecoy/app#412) and fails safe: a forwarded detection with no visitor signature now composes no network identity, on the grounds that no actor beats a false one. Correct — but it means WordPress detections stopped correlating entirely. This restores them.

What is sent

SignalCollector::getClientSignals() builds the material; Detection carries it as cs.

  • Header NAMES only, lowercased and sorted — the name set is what carries the entropy
  • Plus Accept-Language and Accept-Encoding, the two values the server's identity actually reads
  • No other header values. A visitor's cookies and Authorization header never leave the site, while their names are retained so the entropy is intact
  • Proxy/CDN-injected names (cf-*, x-forwarded-*, true-client-ip, cdn-loop, x-vercel-*, fastly-*) excluded, so a visitor fingerprints consistently whether seen here or at a customer's edge

No TLS material. PHP is handed a decrypted request and never sees the ClientHello, so no JA3/JA4 is derivable. The header-name set carries the identity alone — weaker than what an edge sensor produces, and the code says so rather than pretending otherwise.

Tests

tests/ClientSignalsTest.php, 10 cases, pinning both properties that matter:

  • It distinguishes visitors — two different crawlers produce different material; the same one is stable across requests. (Without this the fix would be cosmetic.)
  • It leaks nothing — a request carrying a session cookie, a bearer token and a nonce forwards none of those values. Asserted by serializing the whole payload and searching it, so a value smuggled in under any key is caught, not only the keys the test thought to check.
  • Wire keys hn/al/ae pinned, since they're matched by name across a repository boundary and a rename is silent data loss rather than a build error.

Full suite: 75 passed, 0 failed.

Release

Version bumped to 2.3.1 across webdecoy.php (header + constant), readme.txt (stable tag + changelog) and changelog.txt.

I have not run ./release.sh or pushed anything to R2/wp.org — publishing an update to every install is your call, not a side effect of a code review.

…tions

The plugin beacons detections to the ingest service over its own HTTP
connection, so the request that arrives there carries THIS SITE's outbound
headers, not the visitor's. The service was composing the visitor's network
identity from those — byte-identical on every beacon a given site sends — so
every visitor a site ever reported was folded into one shared "actor".

The platform side of this shipped first and fails safe: a forwarded detection
with no visitor signature now composes no network identity at all, on the
grounds that no actor beats a false one. Correct, but it means WordPress
detections stopped correlating entirely. This restores them.

SignalCollector::getClientSignals() builds the material and Detection carries it
as `cs`. Header NAMES only, plus Accept-Language and Accept-Encoding, which are
the two values the server's identity actually reads — so a visitor's cookies and
Authorization header never leave the site, while the name set that carries the
entropy is preserved intact. Proxy and CDN-injected names (cf-*, x-forwarded-*,
true-client-ip) are excluded so the same visitor fingerprints consistently
whether seen here or at a customer's edge.

No TLS material: PHP is handed a decrypted request and never sees the
ClientHello, so no JA3/JA4 is derivable. The header-name set carries the
identity alone — weaker than what an edge sensor can produce, and honest about
it.

Tested for both properties that matter: that the material actually distinguishes
two different crawlers and is stable for one, and that no header value beyond
those two can leave the site — asserted by serializing the whole payload rather
than checking the keys this test happened to think of.
@cport1
cport1 merged commit 4168a39 into main Jul 26, 2026
3 checks passed
@cport1
cport1 deleted the fix/forward-visitor-client-signals branch July 26, 2026 18:20
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.

1 participant