@@ -98,20 +98,36 @@ fn publisher_page_url(req: &Request<EdgeBody>, publisher_domain: &str) -> String
9898 . and_then ( |value| value. to_str ( ) . ok ( ) )
9999 . filter ( |value| value. len ( ) <= MAX_PUBLISHER_PAGE_URL_BYTES )
100100 else {
101+ log:: debug!(
102+ "Auction page URL: using publisher origin because Referer is absent or invalid"
103+ ) ;
101104 return fallback;
102105 } ;
103- let Ok ( parsed) = Url :: parse ( candidate) else {
106+ let Ok ( mut parsed) = Url :: parse ( candidate) else {
107+ log:: debug!( "Auction page URL: using publisher origin because Referer is not a URL" ) ;
104108 return fallback;
105109 } ;
110+ let publisher_domain = publisher_domain. trim_end_matches ( '.' ) . to_ascii_lowercase ( ) ;
111+ let host_matches = !publisher_domain. is_empty ( )
112+ && parsed. host_str ( ) . is_some_and ( |host| {
113+ let host = host. trim_end_matches ( '.' ) . to_ascii_lowercase ( ) ;
114+ host == publisher_domain
115+ || host
116+ . strip_suffix ( & publisher_domain)
117+ . is_some_and ( |prefix| prefix. ends_with ( '.' ) )
118+ } ) ;
106119 if !matches ! ( parsed. scheme( ) , "http" | "https" )
107- || !parsed
108- . host_str ( )
109- . is_some_and ( |host| host. eq_ignore_ascii_case ( publisher_domain) )
120+ || !host_matches
110121 || !parsed. username ( ) . is_empty ( )
111122 || parsed. password ( ) . is_some ( )
112123 {
124+ log:: debug!(
125+ "Auction page URL: using publisher origin because Referer is not publisher-owned HTTP(S)"
126+ ) ;
113127 return fallback;
114128 }
129+ parsed. set_query ( None ) ;
130+ parsed. set_fragment ( None ) ;
115131 parsed. to_string ( )
116132}
117133
@@ -255,7 +271,7 @@ pub fn convert_tsjs_to_auction_request(
255271/// # Errors
256272///
257273/// Returns an error if:
258- /// - A winning bid is missing a price or render source
274+ /// - A winning bid is missing a price
259275/// - The response serialization fails
260276pub fn convert_to_openrtb_response (
261277 result : & OrchestrationResult ,
@@ -285,7 +301,19 @@ pub fn convert_to_openrtb_response(
285301
286302 // Ordinary markup remains on the mandatory sanitize/rewrite path. A
287303 // typed renderer is serialized separately and never enters the HTML sanitizer.
288- let ( adm, ext) = if let Some ( ref raw_creative) = bid. creative {
304+ let ( adm, ext) = if let Some ( raw_creative) = bid
305+ . creative
306+ . as_deref ( )
307+ . filter ( |creative| !creative. trim ( ) . is_empty ( ) )
308+ {
309+ if bid. renderer . is_some ( ) {
310+ log:: warn!(
311+ "Auction {}: winning bid for slot '{}' from '{}' has both creative markup and a renderer; using creative markup" ,
312+ auction_request. id,
313+ slot_id,
314+ bid. bidder
315+ ) ;
316+ }
289317 let sanitized = creative:: sanitize_creative_html ( raw_creative) ;
290318 let rewritten = creative:: rewrite_creative_html ( settings, & sanitized) ;
291319
@@ -300,20 +328,27 @@ pub fn convert_to_openrtb_response(
300328
301329 ( Some ( rewritten) , None )
302330 } else if let Some ( renderer) = bid. renderer . as_ref ( ) {
303- (
304- None ,
305- BidExt {
306- trusted_server : BidTrustedServerExt { renderer } ,
307- }
308- . to_ext ( ) ,
309- )
331+ let Some ( ext) = ( BidExt {
332+ trusted_server : BidTrustedServerExt { renderer } ,
333+ } )
334+ . to_ext ( ) else {
335+ log:: warn!(
336+ "Auction {}: skipping winning bid for slot '{}' from '{}' because its renderer extension could not be serialized" ,
337+ auction_request. id,
338+ slot_id,
339+ bid. bidder
340+ ) ;
341+ continue ;
342+ } ;
343+ ( None , Some ( ext) )
310344 } else {
311- return Err ( Report :: new ( TrustedServerError :: Auction {
312- message : format ! (
313- "Winning bid for slot '{}' from '{}' has no render source" ,
314- slot_id, bid. bidder
315- ) ,
316- } ) ) ;
345+ log:: warn!(
346+ "Auction {}: skipping winning bid for slot '{}' from '{}' because it has no render source" ,
347+ auction_request. id,
348+ slot_id,
349+ bid. bidder
350+ ) ;
351+ continue ;
317352 } ;
318353
319354 let openrtb_bid = OpenRtbBid {
@@ -657,30 +692,48 @@ mod tests {
657692 }
658693
659694 #[ test]
660- fn publisher_page_url_accepts_only_same_publisher_http_urls ( ) {
661- let request = Request :: builder ( )
662- . uri ( "https://publisher.example.com/auction" )
663- . header (
664- header:: REFERER ,
665- "https://publisher.example.com/article?id=1" ,
666- )
667- . body ( EdgeBody :: empty ( ) )
668- . expect ( "should build request" ) ;
669- assert_eq ! (
670- publisher_page_url( & request, "publisher.example.com" ) ,
671- "https://publisher.example.com/article?id=1"
672- ) ;
673- assert_eq ! (
674- publisher_page_url( & request, "Publisher.Example.Com" ) ,
675- "https://publisher.example.com/article?id=1" ,
676- "DNS host comparison should be case-insensitive"
677- ) ;
695+ fn publisher_page_url_accepts_publisher_hosts_and_strips_private_components ( ) {
696+ for ( candidate, expected) in [
697+ (
698+ "https://publisher.example.com/article?id=1#comments" ,
699+ "https://publisher.example.com/article" ,
700+ ) ,
701+ (
702+ "http://news.publisher.example.com/nested/story?token=secret" ,
703+ "http://news.publisher.example.com/nested/story" ,
704+ ) ,
705+ (
706+ "https://www.publisher.example.com/" ,
707+ "https://www.publisher.example.com/" ,
708+ ) ,
709+ ] {
710+ let request = Request :: builder ( )
711+ . uri ( "https://publisher.example.com/auction" )
712+ . header ( header:: REFERER , candidate)
713+ . body ( EdgeBody :: empty ( ) )
714+ . expect ( "should build request" ) ;
715+ assert_eq ! (
716+ publisher_page_url( & request, "Publisher.Example.Com" ) ,
717+ expected,
718+ "should sanitize publisher-owned Referer"
719+ ) ;
720+ }
721+ }
678722
723+ #[ test]
724+ fn publisher_page_url_rejects_untrusted_or_oversized_referers ( ) {
725+ let oversized = format ! (
726+ "https://publisher.example.com/{}" ,
727+ "x" . repeat( MAX_PUBLISHER_PAGE_URL_BYTES )
728+ ) ;
679729 for candidate in [
680730 "https://other.example/article" ,
731+ "https://publisher.example.com.evil.example/article" ,
732+ "https://evilpublisher.example.com/article" ,
681733 "javascript:alert(1)" ,
682734 "https://user:password@publisher.example.com/article" ,
683735 "not a url" ,
736+ & oversized,
684737 ] {
685738 let request = Request :: builder ( )
686739 . uri ( "https://publisher.example.com/auction" )
@@ -1064,19 +1117,98 @@ mod tests {
10641117 }
10651118
10661119 #[ test]
1067- fn convert_to_openrtb_response_rejects_winner_without_render_source ( ) {
1120+ fn convert_to_openrtb_response_skips_invalid_winners_without_dropping_valid_slots ( ) {
10681121 let settings = make_settings ( ) ;
10691122 let auction_request = make_auction_request ( ) ;
1070- let mut bid = make_bid ( "div-gpt-top" , "appnexus" , Some ( 2.75 ) ) ;
1071- bid. creative = None ;
1123+ let mut missing = make_bid ( "missing" , "invalid" , Some ( 3.0 ) ) ;
1124+ missing. creative = None ;
1125+ let mut whitespace = make_bid ( "whitespace" , "invalid" , Some ( 2.9 ) ) ;
1126+ whitespace. creative = Some ( " \n \t " . to_string ( ) ) ;
1127+ let ordinary = make_bid ( "ordinary" , "appnexus" , Some ( 2.75 ) ) ;
1128+ let mut renderer = make_bid ( "renderer" , "aps" , Some ( 2.5 ) ) ;
1129+ renderer. creative = Some ( " " . to_string ( ) ) ;
1130+ renderer. bid_id = Some ( "upstream-renderer-bid" . to_string ( ) ) ;
1131+ renderer. creative_id = None ;
1132+ renderer. renderer = Some ( BidRenderer :: Aps ( ApsRendererV1 {
1133+ version : 1 ,
1134+ account_id : "example-account" . to_string ( ) ,
1135+ bid_id : "upstream-renderer-bid" . to_string ( ) ,
1136+ creative_id : None ,
1137+ tag_type : ApsTagType :: Iframe ,
1138+ creative_url : "https://creative.example/render" . to_string ( ) ,
1139+ aax_response : "fictional-base64" . to_string ( ) ,
1140+ width : 300 ,
1141+ height : 250 ,
1142+ } ) ) ;
1143+ let result = OrchestrationResult {
1144+ provider_responses : vec ! [ ] ,
1145+ mediator_response : None ,
1146+ winning_bids : HashMap :: from ( [
1147+ ( missing. slot_id . clone ( ) , missing) ,
1148+ ( whitespace. slot_id . clone ( ) , whitespace) ,
1149+ ( ordinary. slot_id . clone ( ) , ordinary) ,
1150+ ( renderer. slot_id . clone ( ) , renderer) ,
1151+ ] ) ,
1152+ total_time_ms : 50 ,
1153+ metadata : HashMap :: new ( ) ,
1154+ } ;
1155+
1156+ let response = convert_to_openrtb_response ( & result, & settings, & auction_request, false )
1157+ . expect ( "should omit invalid winners and preserve valid slots" ) ;
1158+ let json = response_json ( response) ;
1159+ let bids: Vec < & JsonValue > = json[ "seatbid" ]
1160+ . as_array ( )
1161+ . expect ( "should include valid seatbids" )
1162+ . iter ( )
1163+ . map ( |seatbid| & seatbid[ "bid" ] [ 0 ] )
1164+ . collect ( ) ;
1165+
1166+ assert_eq ! ( bids. len( ) , 2 , "should omit only invalid winners" ) ;
1167+ let ordinary = bids
1168+ . iter ( )
1169+ . find ( |bid| bid[ "impid" ] == "ordinary" )
1170+ . expect ( "should preserve ordinary winner" ) ;
1171+ assert_eq ! ( ordinary[ "adm" ] , "<div>Ad</div>" ) ;
1172+ let renderer = bids
1173+ . iter ( )
1174+ . find ( |bid| bid[ "impid" ] == "renderer" )
1175+ . expect ( "should preserve renderer winner" ) ;
1176+ assert ! ( renderer. get( "adm" ) . is_none( ) , "should omit renderer adm" ) ;
1177+ assert ! (
1178+ renderer. get( "crid" ) . is_none( ) ,
1179+ "should omit absent upstream crid"
1180+ ) ;
1181+ assert_eq ! ( renderer[ "id" ] , "upstream-renderer-bid" ) ;
1182+ assert ! ( renderer. get( "ext" ) . is_some( ) , "should include renderer ext" ) ;
1183+ }
1184+
1185+ #[ test]
1186+ fn convert_to_openrtb_response_prefers_creative_when_both_render_sources_exist ( ) {
1187+ let settings = make_settings ( ) ;
1188+ let auction_request = make_auction_request ( ) ;
1189+ let mut bid = make_bid ( "div-gpt-top" , "aps" , Some ( 2.75 ) ) ;
1190+ bid. renderer = Some ( BidRenderer :: Aps ( ApsRendererV1 {
1191+ version : 1 ,
1192+ account_id : "example-account" . to_string ( ) ,
1193+ bid_id : "fictional-bid" . to_string ( ) ,
1194+ creative_id : None ,
1195+ tag_type : ApsTagType :: Iframe ,
1196+ creative_url : "https://creative.example/render" . to_string ( ) ,
1197+ aax_response : "fictional-base64" . to_string ( ) ,
1198+ width : 300 ,
1199+ height : 250 ,
1200+ } ) ) ;
10721201 let result = make_result ( bid) ;
10731202
1074- let error = convert_to_openrtb_response ( & result, & settings, & auction_request, false )
1075- . expect_err ( "should reject bid without a render source" ) ;
1203+ let response = convert_to_openrtb_response ( & result, & settings, & auction_request, false )
1204+ . expect ( "should prefer ordinary creative markup" ) ;
1205+ let json = response_json ( response) ;
1206+ let bid = & json[ "seatbid" ] [ 0 ] [ "bid" ] [ 0 ] ;
10761207
1208+ assert_eq ! ( bid[ "adm" ] , "<div>Ad</div>" ) ;
10771209 assert ! (
1078- format! ( "{error:?} ") . contains ( "has no render source" ) ,
1079- "should explain the missing render source "
1210+ bid . get ( "ext ") . is_none ( ) ,
1211+ "should omit renderer extension when creative markup wins precedence "
10801212 ) ;
10811213 }
10821214
0 commit comments