Skip to content

Commit cd211ff

Browse files
Remove dead ad-proxy URL rewriting from Prebid parse_response (#531)
* Use local request info for Prebid response URL rewriting Store RequestInfo from the original client request on the provider during request_bids and use it in parse_response for URL rewriting. Previously, host and scheme were read back from the upstream Prebid Server response body, allowing a compromised or misconfigured bidder to inject arbitrary values into ad markup URL rewrites. The request_host and request_scheme fields are still sent to Prebid Server in the TrustedServerExt for the signing protocol, but the response-side values are no longer trusted for rewriting. Closes #417 * Remove dead ad-proxy URL rewriting from Prebid parse_response The transform_prebid_response, rewrite_ad_markup, and make_first_party_proxy_url functions generated /ad-proxy/ URLs whose route handler was removed in 25084ba (NextJS with Prebid Integration). The downstream creative::rewrite_creative_html already rewrites all creative URLs to /first-party/proxy, making the Prebid-level rewriting both dead and harmful (it produced double-rewritten URLs pointing to a non-existent endpoint). Removing this dead code also eliminates the security issue where request_host and request_scheme were read from the upstream Prebid Server response body (#417) — there is simply no response-side URL rewriting left to trust or distrust. Closes #417 --------- Co-authored-by: Aram Grigoryan <132480+aram356@users.noreply.github.com>
1 parent 8da4eed commit cd211ff

1 file changed

Lines changed: 2 additions & 162 deletions

File tree

  • crates/trusted-server-core/src/integrations

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

Lines changed: 2 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ use std::sync::Arc;
33
use std::time::Duration;
44

55
use async_trait::async_trait;
6-
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
76
use error_stack::{Report, ResultExt};
87
use fastly::http::{header, Method, StatusCode, Url};
98
use fastly::{Request, Response};
109
use serde::{Deserialize, Serialize};
11-
use serde_json::{json, Value as Json};
10+
use serde_json::Value as Json;
1211
use validator::Validate;
1312

1413
use crate::auction::provider::AuctionProvider;
@@ -417,88 +416,6 @@ fn expand_trusted_server_bidders(
417416
})
418417
.collect()
419418
}
420-
fn transform_prebid_response(
421-
response: &mut Json,
422-
request_host: &str,
423-
request_scheme: &str,
424-
) -> Result<(), Report<TrustedServerError>> {
425-
if let Some(seatbids) = response["seatbid"].as_array_mut() {
426-
for seatbid in seatbids {
427-
if let Some(bids) = seatbid["bid"].as_array_mut() {
428-
for bid in bids {
429-
if let Some(adm) = bid["adm"].as_str() {
430-
bid["adm"] = json!(rewrite_ad_markup(adm, request_host, request_scheme));
431-
}
432-
433-
if let Some(nurl) = bid["nurl"].as_str() {
434-
bid["nurl"] = json!(make_first_party_proxy_url(
435-
nurl,
436-
request_host,
437-
request_scheme,
438-
"track"
439-
));
440-
}
441-
442-
if let Some(burl) = bid["burl"].as_str() {
443-
bid["burl"] = json!(make_first_party_proxy_url(
444-
burl,
445-
request_host,
446-
request_scheme,
447-
"track"
448-
));
449-
}
450-
}
451-
}
452-
}
453-
}
454-
455-
Ok(())
456-
}
457-
458-
fn rewrite_ad_markup(markup: &str, request_host: &str, request_scheme: &str) -> String {
459-
let mut content = markup.to_string();
460-
let cdn_patterns = [
461-
("https://cdn.adsrvr.org", "adsrvr"),
462-
("https://ib.adnxs.com", "adnxs"),
463-
("https://rtb.openx.net", "openx"),
464-
("https://as.casalemedia.com", "casale"),
465-
("https://eus.rubiconproject.com", "rubicon"),
466-
];
467-
468-
for (cdn_url, cdn_name) in cdn_patterns {
469-
if content.contains(cdn_url) {
470-
let proxy_base = format!(
471-
"{}://{}/ad-proxy/{}",
472-
request_scheme, request_host, cdn_name
473-
);
474-
content = content.replace(cdn_url, &proxy_base);
475-
}
476-
}
477-
478-
content = content.replace(
479-
"//cdn.adsrvr.org",
480-
&format!("//{}/ad-proxy/adsrvr", request_host),
481-
);
482-
content = content.replace(
483-
"//ib.adnxs.com",
484-
&format!("//{}/ad-proxy/adnxs", request_host),
485-
);
486-
content
487-
}
488-
489-
fn make_first_party_proxy_url(
490-
third_party_url: &str,
491-
request_host: &str,
492-
request_scheme: &str,
493-
proxy_type: &str,
494-
) -> String {
495-
let encoded = BASE64.encode(third_party_url.as_bytes());
496-
format!(
497-
"{}://{}/ad-proxy/{}/{}",
498-
request_scheme, request_host, proxy_type, encoded
499-
)
500-
}
501-
502419
/// Copies browser headers to the outgoing Prebid Server request.
503420
///
504421
/// In [`ConsentForwardingMode::OpenrtbOnly`] mode, consent cookies are
@@ -1189,7 +1106,7 @@ impl AuctionProvider for PrebidAuctionProvider {
11891106
return Ok(AuctionResponse::error("prebid", response_time_ms));
11901107
}
11911108

1192-
let mut response_json: Json =
1109+
let response_json: Json =
11931110
serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Prebid {
11941111
message: "Failed to parse Prebid response".to_string(),
11951112
})?;
@@ -1205,27 +1122,6 @@ impl AuctionProvider for PrebidAuctionProvider {
12051122
}
12061123
}
12071124

1208-
let request_host = response_json
1209-
.get("ext")
1210-
.and_then(|ext| ext.get("trusted_server"))
1211-
.and_then(|trusted_server| trusted_server.get("request_host"))
1212-
.and_then(|value| value.as_str())
1213-
.unwrap_or("")
1214-
.to_string();
1215-
let request_scheme = response_json
1216-
.get("ext")
1217-
.and_then(|ext| ext.get("trusted_server"))
1218-
.and_then(|trusted_server| trusted_server.get("request_scheme"))
1219-
.and_then(|value| value.as_str())
1220-
.unwrap_or("https")
1221-
.to_string();
1222-
1223-
if request_host.is_empty() {
1224-
log::warn!("Prebid response missing request host; skipping URL rewrites");
1225-
} else {
1226-
transform_prebid_response(&mut response_json, &request_host, &request_scheme)?;
1227-
}
1228-
12291125
let mut auction_response = self.parse_openrtb_response(&response_json, response_time_ms);
12301126
self.enrich_response_metadata(&response_json, &mut auction_response);
12311127

@@ -1570,62 +1466,6 @@ template = "{{client_ip}}:{{user_agent}}"
15701466
);
15711467
}
15721468

1573-
#[test]
1574-
fn transform_prebid_response_rewrites_creatives_and_tracking() {
1575-
let mut response = json!({
1576-
"seatbid": [{
1577-
"bid": [{
1578-
"adm": r#"<img src="https://cdn.adsrvr.org/pixel.png">"#,
1579-
"nurl": "https://notify.example/win",
1580-
"burl": "https://notify.example/bill"
1581-
}]
1582-
}]
1583-
});
1584-
1585-
transform_prebid_response(&mut response, "pub.example", "https")
1586-
.expect("should rewrite response");
1587-
1588-
let rewritten_adm = response["seatbid"][0]["bid"][0]["adm"]
1589-
.as_str()
1590-
.expect("adm should be string");
1591-
assert!(
1592-
rewritten_adm.contains("/ad-proxy/adsrvr"),
1593-
"creative markup should proxy CDN urls"
1594-
);
1595-
1596-
for url_field in ["nurl", "burl"] {
1597-
let value = response["seatbid"][0]["bid"][0][url_field]
1598-
.as_str()
1599-
.expect("should get tracking URL");
1600-
assert!(
1601-
value.contains("/ad-proxy/track/"),
1602-
"tracking URLs should be proxied"
1603-
);
1604-
}
1605-
}
1606-
1607-
#[test]
1608-
fn make_first_party_proxy_url_base64_encodes_target() {
1609-
let url = "https://cdn.example/path?x=1";
1610-
let rewritten = make_first_party_proxy_url(url, "pub.example", "https", "track");
1611-
assert!(
1612-
rewritten.starts_with("https://pub.example/ad-proxy/track/"),
1613-
"proxy prefix should be applied"
1614-
);
1615-
1616-
let encoded = rewritten
1617-
.split("/ad-proxy/track/")
1618-
.nth(1)
1619-
.expect("should have encoded payload after proxy prefix");
1620-
let decoded = BASE64
1621-
.decode(encoded.as_bytes())
1622-
.expect("should decode base64 proxy payload");
1623-
assert_eq!(
1624-
String::from_utf8(decoded).expect("should be valid UTF-8"),
1625-
url
1626-
);
1627-
}
1628-
16291469
#[test]
16301470
fn matches_script_url_matches_common_variants() {
16311471
let integration = PrebidIntegration::new(base_config());

0 commit comments

Comments
 (0)