Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard#822
Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard#822aram356 wants to merge 3 commits into
Conversation
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
left a comment
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.
| /// for a cross-origin CDN about first-party serving. The match is anchored on | ||
| /// the semantic `.pmOrigin)` close of the guard; group 1 is the (possibly | ||
| /// minified) event identifier and group 2 the `…pmOrigin` operand. | ||
| static SP_MESSAGE_ORIGIN_GUARD_PATTERN: LazyLock<Regex> = LazyLock::new(|| { |
There was a problem hiding this comment.
Non-blocking: same-origin guard rewrite may miss the documented/current wrapper guard shape
This rewrite only matches guards that end in .pmOrigin). The documented Sourcepoint wrapper path (/wrapperMessagingWithoutDetection.js) currently has a different handler shape (if (<event>.origin === <origin-var>)), where the origin variable is derived from baseEndpoint. Under this proxy, that value can be /integrations/sourcepoint/cdn, while MessageEvent.origin is the bare publisher origin, so the consent-message postMessages can still be rejected for that wrapper.
Suggested fix: add a second narrowly anchored rewrite for the legacy/current if (<event>.origin === <origin-var>) handler, ideally anchored near Sourcepoint-specific message-handler tokens, and cover it with a realistic wrapper snippet test.
| ); | ||
| } | ||
|
|
||
| fn rewrite_html_response(&self, response: &mut Response<EdgeBody>, rewritten: String) { |
There was a problem hiding this comment.
Non-blocking: rewritten HTML responses now inherit the JS public-cache policy
rewrite_html_response() uses the shared finalizer, which overwrites non-Set-Cookie responses with Cache-Control: public, max-age=<ttl>. That policy was appropriate for rewritten versioned JS/static assets, but the new HTML rewrite targets unversioned documents like /us_pm/index.html; it also bypasses the existing forwarded_cookies-aware apply_cache_headers path.
Suggested fix: split JS and HTML finalization/cache policy. For rewritten HTML, preserve upstream Cache-Control when present; if absent, apply the existing apply_cache_headers(response, forwarded_cookies) behavior. Add tests for preserving no-store and for private caching when cookies were forwarded.
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.