Skip to content

Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard#822

Merged
aram356 merged 6 commits into
mainfrom
fix-sourcepoint-publicpath-404
Jun 29, 2026
Merged

Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard#822
aram356 merged 6 commits into
mainfrom
fix-sourcepoint-publicpath-404

Conversation

@aram356

@aram356 aram356 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #821

Stacked on #819 — base branch is fix-integration-relative-urls. Review/merge after #819 (or rebase onto main once #819 lands). (Branch name is legacy — the fix is HTML asset paths + the message-origin guard, not webpack publicPath.)

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. On cdn.privacy-mgmt.com those 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/href with /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> stayed position:fixed. The wrapper validates messages from its own iframe with e.origin === params.msgOrigin || e.origin === params.pmOrigin, where msgOrigin is baseEndpoint used verbatim. First-party, baseEndpoint is a path (/integrations/sourcepoint/cdn), so msgOrigin = https://<publisher>/integrations/sourcepoint/cdn — which never equals the iframe's bare origin https://<publisher>. So the wrapper adds html.sp-message-open (locks scroll) but never shows the dialog or removes the lock.

Verified dead ends: an absolute baseEndpoint keeps the path → still no match; a bare-origin baseEndpoint needs 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)

  • Assets PrivacyManagerUS.* / polyfills.* → 200.
  • Consent dialog ("PRIVACY AND LEGAL TERMS / Agree & Continue") renders.
  • Clicking Agree & Continuesp-message-open removed → body back to staticpage 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.

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.
@aram356 aram356 assigned aram356 and unassigned aram356 Jun 25, 2026
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.
@aram356 aram356 force-pushed the fix-sourcepoint-publicpath-404 branch from 521359a to 297dc93 Compare June 25, 2026 08:18
@aram356 aram356 changed the title Fix Sourcepoint scroll lock: rewrite empty webpack publicPath to first-party prefix Fix Sourcepoint privacy-manager 404: rewrite root-absolute asset paths in proxied iframe HTML Jun 25, 2026
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.
@aram356 aram356 changed the title Fix Sourcepoint privacy-manager 404: rewrite root-absolute asset paths in proxied iframe HTML Fix Sourcepoint first-party consent: iframe asset 404s + same-origin message guard Jun 25, 2026
@aram356 aram356 self-assigned this Jun 25, 2026
@aram356 aram356 marked this pull request as draft June 25, 2026 17:05
@aram356 aram356 marked this pull request as ready for review June 25, 2026 17:14

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread crates/trusted-server-core/src/integrations/sourcepoint.rs
Comment thread crates/trusted-server-core/src/integrations/sourcepoint.rs Outdated

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍 LGTM

Base automatically changed from fix-integration-relative-urls to main June 29, 2026 06:19
aram356 added 2 commits June 28, 2026 23:25
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
@aram356 aram356 merged commit 4f55104 into main Jun 29, 2026
14 checks passed
@aram356 aram356 deleted the fix-sourcepoint-publicpath-404 branch June 29, 2026 06:39
prk-Jr added a commit that referenced this pull request Jun 29, 2026
Resolve conflicts from main's PR16 axum adapter (#643) and first-party
consent fixes (#819, #822) by keeping this branch's PR18 legacy-admin-alias
credential-leak fix across all three adapters. Core changes from main merged
cleanly; lockfiles and manifests already consistent.
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.
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.

Sourcepoint privacy-manager iframe assets 404: root-absolute paths not rewritten

3 participants