Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard#822
Merged
Conversation
The GPT, DataDome, Permutive, Lockr, and Sourcepoint attribute rewriters
rewrote a publisher's `<script src>` / `<link href>` to an absolute first-party
URL built from `request_host`, e.g. `https://{request_host}/integrations/gpt/script`.
When the host that reaches the program isn't the page's host — behind a
host-rewriting dev proxy, or any deployment where `request_host` resolves to the
routing host rather than the production domain — those URLs leak the wrong host
into the page (e.g. `https://ts.example.com/integrations/...` while the address
bar shows `www.example.com`).
Emit them root-relative (`/integrations/...`) instead, so the browser resolves
them against the page's own host. This is correct both in production and behind
a proxy, removes the dependency on `request_host`/`X-Forwarded-Host` derivation,
and matches what GTM, Didomi, Testlight, and these integrations' own inline-script
rewriters already do.
Audited every integration: these five were the only page-facing first-party URLs
built from a host. NextJS RSC/flight payload rewriting stays absolute (those URLs
are intentional). The DataDome protection module's raw-`Host` read is server-side
(validation API body), not page-facing, and is left as-is. Tests updated to
assert the root-relative form.
The privacy-manager iframe documents (e.g. us_pm/index.html) reference their assets with root-absolute paths: `<script src="/PrivacyManagerUS.<hash>.js">`, `/polyfills.<hash>.js`, `/manifest.json`. On cdn.privacy-mgmt.com those resolve to the CDN; served first-party through Trusted Server the iframe origin is the publisher, so `/PrivacyManagerUS.<hash>.js` resolves to the publisher root and 404s — the privacy-manager UI cannot load its code. Add an HTML response rewrite (mirroring the existing JavaScript rewrite path) that prefixes root-absolute `src`/`href` values with /integrations/sourcepoint/cdn so the iframe assets load through the proxy. Protocol-relative (`//host`) and absolute (`https://…`) URLs are left untouched. The proxy now also requests identity encoding for likely-HTML paths so the body is readable, and a shared finalize_rewritten_response helper sets the right content type for each. Verified in a browser through the proxy: PrivacyManagerUS.*/polyfills.* go from 404 to 200 and the privacy-manager app renders.
521359a to
297dc93
Compare
With assets loading, the consent dialog still never appeared and the page stayed scroll-locked. Root cause: the wrapper validates messages from its own message/privacy-manager iframe with `e.origin === params.msgOrigin || e.origin === params.pmOrigin`, where `msgOrigin` is `baseEndpoint` used verbatim. Under first-party proxying `baseEndpoint` is a path (`/integrations/sourcepoint/cdn`), so `msgOrigin` becomes `https://<publisher>/integrations/sourcepoint/cdn` — which never equals the iframe's bare origin `https://<publisher>`. The guard rejects the iframe's `sp.showMessage`/choice messages, so the wrapper adds `html.sp-message-open` (locking scroll) but never shows the dialog or removes the lock: the page renders but cannot scroll, behind an invisible consent gate. An absolute `baseEndpoint` can't fix this — `msgOrigin` keeps the path prefix and still never equals the bare origin — and serving Sourcepoint under a bare origin would require a dedicated subdomain or claiming colliding root paths. Since the message iframe is genuinely same-origin when proxied first-party, add a third script rewrite that lets the guard also accept `e.origin === location.origin`. This only additionally trusts a same-origin frame (which already has full page access), so it adds no attack surface; it adapts an origin check written for a cross-origin CDN to first-party serving. The rewrite is anchored on the semantic `.pmOrigin)` guard close and captures the minified event identifier. Verified in a browser through the proxy: the consent dialog renders, "Agree & Continue" dismisses it, `sp-message-open` is removed, and the page scrolls.
ChristianPavilonis
approved these changes
Jun 25, 2026
ChristianPavilonis
left a comment
Collaborator
There was a problem hiding this comment.
Review Summary
Approved. The change is narrowly scoped and the reported CI checks are passing. I left two non-blocking findings inline: both are worth addressing, but I do not think they need to block this PR if you prefer to follow up separately.
Split rewritten-response finalization so HTML iframe documents no longer inherit the versioned-JS public cache policy. Rewritten JavaScript bundles keep the fixed `public, max-age=<ttl>` policy (static, hashed filenames); rewritten HTML (e.g. `us_pm/index.html`) is unversioned, so it now defers to `apply_cache_headers`, which preserves an upstream `Cache-Control` when present and otherwise applies the cookie-aware default (private when cookies were forwarded). Generalize the message-origin guard rewrite to also match a standalone `<event>.origin===<obj>.msgOrigin)` guard (the `wrapperMessagingWithoutDetection.js` shape, which compares only against the single baseEndpoint-derived `msgOrigin`), in addition to the combined `…msgOrigin || …pmOrigin)` form. Both anchors are Sourcepoint-specific field names, so generic origin checks are untouched and the combined form is still rewritten exactly once. Add tests for HTML cache preservation/privacy, the standalone msgOrigin guard, and the no-double-match invariant on the combined guard.
…cpath-404 # Conflicts: # crates/trusted-server-core/src/integrations/datadome.rs # crates/trusted-server-core/src/integrations/gpt.rs # crates/trusted-server-core/src/integrations/lockr.rs # crates/trusted-server-core/src/integrations/permutive.rs # crates/trusted-server-core/src/integrations/sourcepoint.rs
prk-Jr
added a commit
that referenced
this pull request
Jun 29, 2026
Keep the branch's evolved tree (fastly 0.12, worker 0.8, edgezero ce6bcf74, Spin adapter), which already supersedes main's PR16/PR18 content carried via the stack's merge history. Incorporate main's two new fixes that the branch lacked: - #819 root-relative first-party integration URLs (datadome, gpt, lockr, permutive, sourcepoint) - #822 Sourcepoint first-party consent iframe/same-origin guard Auto-merge picked main's older fastly-0.11 TLS code for the fastly adapter (main.rs, platform.rs, route_tests.rs); restore the branch versions so the get_tls_protocol/cipher calls match the locked fastly 0.12 API. Verified: cargo check-{fastly,cloudflare,axum,spin} --locked all pass.
14 tasks
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.
Closes #821
Makes Sourcepoint's CMP actually work through Trusted Server's first-party proxy: the consent dialog renders and the page scrolls. Two distinct first-party-proxy breakages, both verified in a real browser through the proxy.
1. Privacy-manager iframe assets 404
The privacy-manager iframe documents (
us_pm/index.html) reference assets root-absolute:<script src="/PrivacyManagerUS.<hash>.js">,/polyfills.<hash>.js,/manifest.json. Oncdn.privacy-mgmt.comthose hit the CDN; served first-party the iframe origin is the publisher, so they resolve to the publisher root → 404, and the privacy-manager UI can't load.Fix: an HTML response rewrite that prefixes root-absolute
src/hrefwith/integrations/sourcepoint/cdn(protocol-relative/absolute URLs untouched). Verified 404 → 200.2. Consent dialog never shows / page stays scroll-locked
Even with assets at 200, the dialog stayed hidden and
<body>stayedposition:fixed. The wrapper validates messages from its own iframe withe.origin === params.msgOrigin || e.origin === params.pmOrigin, wheremsgOriginisbaseEndpointused verbatim. First-party,baseEndpointis a path (/integrations/sourcepoint/cdn), somsgOrigin = https://<publisher>/integrations/sourcepoint/cdn— which never equals the iframe's bare originhttps://<publisher>. So the wrapper addshtml.sp-message-open(locks scroll) but never shows the dialog or removes the lock.Verified dead ends: an absolute
baseEndpointkeeps the path → still no match; a bare-originbaseEndpointneeds a dedicated subdomain (not available) or claiming colliding site-root paths (/manifest.json,/polyfills.*).Fix: since the iframe is genuinely same-origin when proxied first-party, a third script rewrite lets the guard also accept
e.origin === location.origin. This only additionally trusts a same-origin frame — which already has full page access — so it adds no attack surface; it adapts an origin check written for a cross-origin CDN to first-party serving. Anchored on the semantic.pmOrigin)close, capturing the minified event identifier; it only adds an OR branch (existing checks untouched).Verification (in-browser, through the proxy)
PrivacyManagerUS.*/polyfills.*→ 200.sp-message-openremoved →bodyback tostatic→ page scrolls (verified scrollTop moves, was stuck at 0).Tests
rewrites_root_absolute_asset_paths_in_html,html_rewrite_preserves_absolute_and_protocol_relative_urls,is_likely_html_path_matches_iframe_documents,rewrites_message_origin_guard_to_accept_same_origin,message_origin_guard_rewrite_handles_minified_identifiers,message_origin_guard_rewrite_leaves_unrelated_origin_checks_untouched. All 48 Sourcepoint tests pass; fmt/clippy/check clean.Note on fragility
Both rewrites pattern-match Sourcepoint's minified JS/HTML (as the pre-existing CDN-URL and
/unified/rewrites already do), so a Sourcepoint recompile could require updating an anchor. Anchored on semantic tokens (src/href,.pmOrigin) to minimise that.