Skip to content

Commit bc54caa

Browse files
authored
Make first-party integration URLs root-relative (#819)
* Make first-party integration URLs root-relative 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. * Address PR review: fix DataDome doc example and note base-href tradeoff
1 parent 5447648 commit bc54caa

5 files changed

Lines changed: 50 additions & 61 deletions

File tree

crates/trusted-server-core/src/integrations/datadome.rs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
//! automatically rewrite `DataDome` script URLs in HTML responses:
5353
//!
5454
//! - `<script src="https://js.datadome.co/tags.js">` becomes
55-
//! `<script src="https://publisher.com/integrations/datadome/tags.js">`
55+
//! `<script src="/integrations/datadome/tags.js">`
5656
//! - Handles both `src` and `href` attributes (for preload/prefetch links)
5757
5858
use std::sync::{Arc, LazyLock};
@@ -803,7 +803,7 @@ impl IntegrationAttributeRewriter for DataDomeIntegration {
803803
&self,
804804
_attr_name: &str,
805805
attr_value: &str,
806-
ctx: &IntegrationAttributeContext<'_>,
806+
_ctx: &IntegrationAttributeContext<'_>,
807807
) -> AttributeRewriteAction {
808808
// Check if this is a DataDome script URL
809809
let is_datadome =
@@ -814,10 +814,12 @@ impl IntegrationAttributeRewriter for DataDomeIntegration {
814814
}
815815

816816
let path = Self::extract_datadome_path(attr_value);
817-
let new_url = format!(
818-
"{}://{}/integrations/datadome{}",
819-
ctx.request_scheme, ctx.request_host, path
820-
);
817+
// Root-relative so the browser resolves it against the page host.
818+
// Note: a page-level `<base href>` participates in this resolution, so
819+
// on pages that set an external base URL these resolve against that base
820+
// rather than the address-bar origin — an accepted tradeoff, matching
821+
// GTM/Didomi/Testlight which are also relative.
822+
let new_url = format!("/integrations/datadome{path}");
821823

822824
log::info!(
823825
"[datadome] Rewriting script src from {} to {}",
@@ -1328,10 +1330,7 @@ mod tests {
13281330
let action = integration.rewrite("src", "https://js.datadome.co/tags.js", &ctx);
13291331
match action {
13301332
AttributeRewriteAction::Replace(new_url) => {
1331-
assert_eq!(
1332-
new_url,
1333-
"https://publisher.com/integrations/datadome/tags.js"
1334-
);
1333+
assert_eq!(new_url, "/integrations/datadome/tags.js");
13351334
}
13361335
_ => panic!("Expected Replace action"),
13371336
}
@@ -1340,10 +1339,7 @@ mod tests {
13401339
let action = integration.rewrite("href", "https://js.datadome.co/tags.js", &ctx);
13411340
match action {
13421341
AttributeRewriteAction::Replace(new_url) => {
1343-
assert_eq!(
1344-
new_url,
1345-
"https://publisher.com/integrations/datadome/tags.js"
1346-
);
1342+
assert_eq!(new_url, "/integrations/datadome/tags.js");
13471343
}
13481344
_ => panic!("Expected Replace action for href"),
13491345
}
@@ -1368,10 +1364,7 @@ mod tests {
13681364
let action = integration.rewrite("src", "https://js.datadome.co/js/check", &ctx);
13691365
match action {
13701366
AttributeRewriteAction::Replace(new_url) => {
1371-
assert_eq!(
1372-
new_url,
1373-
"https://publisher.com/integrations/datadome/js/check"
1374-
);
1367+
assert_eq!(new_url, "/integrations/datadome/js/check");
13751368
}
13761369
_ => panic!("Expected Replace action"),
13771370
}
@@ -1380,10 +1373,7 @@ mod tests {
13801373
let action = integration.rewrite("href", "//js.datadome.co/js/signal", &ctx);
13811374
match action {
13821375
AttributeRewriteAction::Replace(new_url) => {
1383-
assert_eq!(
1384-
new_url,
1385-
"https://publisher.com/integrations/datadome/js/signal"
1386-
);
1376+
assert_eq!(new_url, "/integrations/datadome/js/signal");
13871377
}
13881378
_ => panic!("Expected Replace action for protocol-relative URL"),
13891379
}
@@ -1392,10 +1382,7 @@ mod tests {
13921382
let action = integration.rewrite("src", "https://js.datadome.co", &ctx);
13931383
match action {
13941384
AttributeRewriteAction::Replace(new_url) => {
1395-
assert_eq!(
1396-
new_url,
1397-
"https://publisher.com/integrations/datadome/tags.js"
1398-
);
1385+
assert_eq!(new_url, "/integrations/datadome/tags.js");
13991386
}
14001387
_ => panic!("Expected Replace action for bare domain"),
14011388
}

crates/trusted-server-core/src/integrations/gpt.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -435,17 +435,19 @@ impl IntegrationAttributeRewriter for GptIntegration {
435435
&self,
436436
_attr_name: &str,
437437
attr_value: &str,
438-
ctx: &IntegrationAttributeContext<'_>,
438+
_ctx: &IntegrationAttributeContext<'_>,
439439
) -> AttributeRewriteAction {
440440
if !self.config.rewrite_script {
441441
return AttributeRewriteAction::keep();
442442
}
443443

444444
if Self::is_gpt_script_url(attr_value) {
445-
AttributeRewriteAction::replace(format!(
446-
"{}://{}/integrations/gpt/script",
447-
ctx.request_scheme, ctx.request_host
448-
))
445+
// Root-relative so the browser resolves it against the page host.
446+
// Note: a page-level `<base href>` participates in this resolution,
447+
// so on pages that set an external base URL these resolve against
448+
// that base rather than the address-bar origin — an accepted
449+
// tradeoff, matching GTM/Didomi/Testlight which are also relative.
450+
AttributeRewriteAction::replace("/integrations/gpt/script".to_string())
449451
} else {
450452
AttributeRewriteAction::keep()
451453
}
@@ -587,8 +589,8 @@ mod tests {
587589
match result {
588590
AttributeRewriteAction::Replace(url) => {
589591
assert_eq!(
590-
url, "https://edge.example.com/integrations/gpt/script",
591-
"should rewrite to first-party script endpoint"
592+
url, "/integrations/gpt/script",
593+
"should rewrite to root-relative first-party script endpoint"
592594
);
593595
}
594596
other => panic!("Expected Replace action, got {:?}", other),

crates/trusted-server-core/src/integrations/lockr.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,17 +412,19 @@ impl IntegrationAttributeRewriter for LockrIntegration {
412412
&self,
413413
_attr_name: &str,
414414
attr_value: &str,
415-
ctx: &IntegrationAttributeContext<'_>,
415+
_ctx: &IntegrationAttributeContext<'_>,
416416
) -> AttributeRewriteAction {
417417
if !self.config.rewrite_sdk {
418418
return AttributeRewriteAction::Keep;
419419
}
420420

421421
if self.is_lockr_sdk_url(attr_value) {
422-
let replacement = format!(
423-
"{}://{}/integrations/lockr/sdk",
424-
ctx.request_scheme, ctx.request_host
425-
);
422+
// Root-relative so the browser resolves it against the page host.
423+
// Note: a page-level `<base href>` participates in this resolution,
424+
// so on pages that set an external base URL these resolve against
425+
// that base rather than the address-bar origin — an accepted
426+
// tradeoff, matching GTM/Didomi/Testlight which are also relative.
427+
let replacement = "/integrations/lockr/sdk".to_string();
426428
log::debug!("Rewriting Lockr SDK URL to {}", replacement);
427429
AttributeRewriteAction::Replace(replacement)
428430
} else {
@@ -532,10 +534,8 @@ mod tests {
532534

533535
assert_eq!(
534536
result,
535-
AttributeRewriteAction::Replace(
536-
"https://edge.example.com/integrations/lockr/sdk".to_string()
537-
),
538-
"should rewrite Lockr SDK URL to first-party proxy"
537+
AttributeRewriteAction::Replace("/integrations/lockr/sdk".to_string()),
538+
"should rewrite Lockr SDK URL to root-relative first-party proxy"
539539
);
540540
}
541541

crates/trusted-server-core/src/integrations/permutive.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,18 +425,20 @@ impl IntegrationAttributeRewriter for PermutiveIntegration {
425425
&self,
426426
_attr_name: &str,
427427
attr_value: &str,
428-
ctx: &IntegrationAttributeContext<'_>,
428+
_ctx: &IntegrationAttributeContext<'_>,
429429
) -> AttributeRewriteAction {
430430
if !self.config.rewrite_sdk {
431431
return AttributeRewriteAction::keep();
432432
}
433433

434434
if self.is_permutive_sdk_url(attr_value) {
435-
// Rewrite to first-party SDK endpoint
436-
AttributeRewriteAction::replace(format!(
437-
"{}://{}/integrations/permutive/sdk",
438-
ctx.request_scheme, ctx.request_host
439-
))
435+
// Rewrite to first-party SDK endpoint.
436+
// Root-relative so the browser resolves it against the page host.
437+
// Note: a page-level `<base href>` participates in this resolution,
438+
// so on pages that set an external base URL these resolve against
439+
// that base rather than the address-bar origin — an accepted
440+
// tradeoff, matching GTM/Didomi/Testlight which are also relative.
441+
AttributeRewriteAction::replace("/integrations/permutive/sdk".to_string())
440442
} else {
441443
AttributeRewriteAction::keep()
442444
}
@@ -547,7 +549,7 @@ mod tests {
547549

548550
assert!(matches!(rewritten, AttributeRewriteAction::Replace(_)));
549551
if let AttributeRewriteAction::Replace(url) = rewritten {
550-
assert_eq!(url, "https://edge.example.com/integrations/permutive/sdk");
552+
assert_eq!(url, "/integrations/permutive/sdk");
551553
}
552554
}
553555

crates/trusted-server-core/src/integrations/sourcepoint.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,7 @@ impl SourcepointIntegration {
285285
Ok(target.to_string())
286286
}
287287

288-
fn build_first_party_url(
289-
&self,
290-
source_url: &str,
291-
ctx: &IntegrationAttributeContext<'_>,
292-
) -> Option<String> {
288+
fn build_first_party_url(&self, source_url: &str) -> Option<String> {
293289
let parsed = parse_sourcepoint_url(source_url)?;
294290
if parsed.host_str()? != SOURCEPOINT_CDN_HOST {
295291
return None;
@@ -301,10 +297,12 @@ impl SourcepointIntegration {
301297
.map(|value| format!("?{value}"))
302298
.unwrap_or_default();
303299

304-
Some(format!(
305-
"{}://{}{}{}{}",
306-
ctx.request_scheme, ctx.request_host, SOURCEPOINT_CDN_PREFIX, path, query
307-
))
300+
// Root-relative so the browser resolves it against the page host.
301+
// Note: a page-level `<base href>` participates in this resolution, so
302+
// on pages that set an external base URL these resolve against that base
303+
// rather than the address-bar origin — an accepted tradeoff, matching
304+
// GTM/Didomi/Testlight which are also relative.
305+
Some(format!("{SOURCEPOINT_CDN_PREFIX}{path}{query}"))
308306
}
309307

310308
fn copy_headers(
@@ -851,11 +849,11 @@ impl IntegrationAttributeRewriter for SourcepointIntegration {
851849
&self,
852850
_attr_name: &str,
853851
attr_value: &str,
854-
ctx: &IntegrationAttributeContext<'_>,
852+
_ctx: &IntegrationAttributeContext<'_>,
855853
) -> AttributeRewriteAction {
856854
// `handles_attribute()` already gates on `rewrite_sdk`, so this
857855
// method is only called when rewriting is enabled.
858-
if let Some(rewritten) = self.build_first_party_url(attr_value, ctx) {
856+
if let Some(rewritten) = self.build_first_party_url(attr_value) {
859857
return AttributeRewriteAction::replace(rewritten);
860858
}
861859

@@ -986,7 +984,7 @@ mod tests {
986984
assert_eq!(
987985
rewritten,
988986
AttributeRewriteAction::replace(
989-
"https://edge.example.com/integrations/sourcepoint/cdn/mms/v2/get_site_data?account_id=821",
987+
"/integrations/sourcepoint/cdn/mms/v2/get_site_data?account_id=821",
990988
)
991989
);
992990
}

0 commit comments

Comments
 (0)