@@ -217,7 +217,8 @@ pub fn convert_tsjs_to_auction_request(
217217
218218/// Convert `OrchestrationResult` to `OpenRTB` response format.
219219///
220- /// Returns rewritten creative HTML directly in the `adm` field for inline delivery.
220+ /// Always sanitizes creative HTML in the `adm` field and optionally rewrites it
221+ /// according to the auction configuration.
221222///
222223/// # Errors
223224///
@@ -250,21 +251,34 @@ pub fn convert_to_openrtb_response(
250251 let width = to_openrtb_i32 ( bid. width , "width" , & bid_context) ;
251252 let height = to_openrtb_i32 ( bid. height , "height" , & bid_context) ;
252253
253- // Process creative HTML if present - — sanitize dangerous markup first, then rewrite URLs .
254+ // Process creative HTML if present — always sanitize dangerous markup first.
254255 let creative_html = if let Some ( ref raw_creative) = bid. creative {
255256 let sanitized = creative:: sanitize_creative_html ( raw_creative) ;
256- let rewritten = creative:: rewrite_creative_html ( settings, & sanitized) ;
257+ let sanitized_len = sanitized. len ( ) ;
258+ let rewrite_creatives = settings. auction . rewrite_creatives ;
259+ let processed = if rewrite_creatives {
260+ creative:: rewrite_creative_html ( settings, & sanitized)
261+ } else {
262+ sanitized
263+ } ;
264+ let rewrite_mode = if rewrite_creatives {
265+ "enabled"
266+ } else {
267+ "disabled"
268+ } ;
257269
258270 log:: debug!(
259- "Processed creative for auction {} slot {} ({} → {} → {} bytes)" ,
271+ "Processed creative for auction {} slot {} bidder {} (rewrite {}, raw {} bytes, sanitized {} bytes, output {} bytes)" ,
260272 auction_request. id,
261273 slot_id,
274+ bid. bidder,
275+ rewrite_mode,
262276 raw_creative. len( ) ,
263- sanitized . len ( ) ,
264- rewritten . len( )
277+ sanitized_len ,
278+ processed . len( )
265279 ) ;
266280
267- rewritten
281+ processed
268282 } else {
269283 // No creative provided (e.g., from mediation layer that returns iframe URLs)
270284 log:: warn!(
@@ -445,6 +459,15 @@ mod tests {
445459 }
446460 }
447461
462+ fn make_complete_creative_bid ( ) -> Bid {
463+ let mut bid = make_bid ( "div-gpt-top" , "appnexus" , Some ( 2.75 ) ) ;
464+ bid. creative = Some (
465+ r#"<html><body><a href="https://advertiser.example.com/landing"><img src="https://cdn.example.com/ad.png" style="background-image:url(https://styles.example.com/bg.png)" onerror="auction-handler-marker()"></a><script>auction-script-marker</script></body></html>"#
466+ . to_string ( ) ,
467+ ) ;
468+ bid
469+ }
470+
448471 fn make_result ( bid : Bid ) -> OrchestrationResult {
449472 OrchestrationResult {
450473 provider_responses : vec ! [ AuctionResponse {
@@ -466,6 +489,13 @@ mod tests {
466489 . expect ( "should parse JSON response" )
467490 }
468491
492+ fn response_adm ( response : Response < EdgeBody > ) -> String {
493+ response_json ( response) [ "seatbid" ] [ 0 ] [ "bid" ] [ 0 ] [ "adm" ]
494+ . as_str ( )
495+ . expect ( "should serialize adm as a string" )
496+ . to_string ( )
497+ }
498+
469499 fn make_banner_body ( config : Option < JsonValue > ) -> AdRequest {
470500 AdRequest {
471501 ad_units : vec ! [ AdUnit {
@@ -932,6 +962,103 @@ mod tests {
932962 ) ;
933963 }
934964
965+ #[ test]
966+ fn convert_to_openrtb_response_rewrites_sanitized_creative_by_default ( ) {
967+ let settings = make_settings ( ) ;
968+ let auction_request = make_auction_request ( ) ;
969+ let result = make_result ( make_complete_creative_bid ( ) ) ;
970+
971+ let response = convert_to_openrtb_response ( & result, & settings, & auction_request, false )
972+ . expect ( "should convert creative with rewriting enabled" ) ;
973+ let adm = response_adm ( response) ;
974+
975+ assert ! (
976+ adm. matches( "/first-party/proxy?tsurl=" ) . count( ) >= 2 ,
977+ "should rewrite image and inline CSS URLs through the proxy: {adm}"
978+ ) ;
979+ assert ! (
980+ adm. contains( "/first-party/click?tsurl=" ) ,
981+ "should rewrite click URLs: {adm}"
982+ ) ;
983+ assert ! (
984+ adm. contains( "data-tsclick" ) ,
985+ "should add the click guard attribute: {adm}"
986+ ) ;
987+ assert ! (
988+ adm. contains( "tsjs-unified.min.js" ) ,
989+ "should inject the unified creative runtime: {adm}"
990+ ) ;
991+ assert ! (
992+ !adm. contains( r#"src="https://cdn.example.com/ad.png""# ) ,
993+ "should not retain the image URL as a direct attribute: {adm}"
994+ ) ;
995+ assert ! (
996+ !adm. contains( r#"href="https://advertiser.example.com/landing""# ) ,
997+ "should not retain the click URL as a direct attribute: {adm}"
998+ ) ;
999+ assert ! (
1000+ !adm. contains( "url(https://styles.example.com/bg.png)" ) ,
1001+ "should not retain the CSS URL as a direct value: {adm}"
1002+ ) ;
1003+ assert ! (
1004+ !adm. contains( "auction-script-marker" ) ,
1005+ "should remove malicious script content before rewriting: {adm}"
1006+ ) ;
1007+ assert ! (
1008+ !adm. contains( "auction-handler-marker" ) && !adm. contains( "onerror" ) ,
1009+ "should remove event handlers before rewriting: {adm}"
1010+ ) ;
1011+ }
1012+
1013+ #[ test]
1014+ fn convert_to_openrtb_response_can_skip_rewriting_but_not_sanitization ( ) {
1015+ let mut settings = make_settings ( ) ;
1016+ settings. auction . rewrite_creatives = false ;
1017+ let auction_request = make_auction_request ( ) ;
1018+ let result = make_result ( make_complete_creative_bid ( ) ) ;
1019+
1020+ let response = convert_to_openrtb_response ( & result, & settings, & auction_request, false )
1021+ . expect ( "should convert creative with rewriting disabled" ) ;
1022+ let adm = response_adm ( response) ;
1023+
1024+ assert ! (
1025+ adm. contains( r#"src="https://cdn.example.com/ad.png""# ) ,
1026+ "should retain the sanitizer-accepted image URL: {adm}"
1027+ ) ;
1028+ assert ! (
1029+ adm. contains( r#"href="https://advertiser.example.com/landing""# ) ,
1030+ "should retain the sanitizer-accepted click URL: {adm}"
1031+ ) ;
1032+ assert ! (
1033+ adm. contains( "url(https://styles.example.com/bg.png)" ) ,
1034+ "should retain the sanitizer-accepted CSS URL: {adm}"
1035+ ) ;
1036+ assert ! (
1037+ !adm. contains( "/first-party/proxy" ) ,
1038+ "should not rewrite resource URLs: {adm}"
1039+ ) ;
1040+ assert ! (
1041+ !adm. contains( "/first-party/click" ) ,
1042+ "should not rewrite click URLs: {adm}"
1043+ ) ;
1044+ assert ! (
1045+ !adm. contains( "data-tsclick" ) ,
1046+ "should not add the click guard attribute: {adm}"
1047+ ) ;
1048+ assert ! (
1049+ !adm. contains( "tsjs-unified.min.js" ) ,
1050+ "should not inject the unified creative runtime: {adm}"
1051+ ) ;
1052+ assert ! (
1053+ !adm. contains( "auction-script-marker" ) ,
1054+ "should still remove malicious script content: {adm}"
1055+ ) ;
1056+ assert ! (
1057+ !adm. contains( "auction-handler-marker" ) && !adm. contains( "onerror" ) ,
1058+ "should still remove event handlers: {adm}"
1059+ ) ;
1060+ }
1061+
9351062 #[ test]
9361063 fn convert_to_openrtb_response_serializes_missing_creative_as_empty_adm ( ) {
9371064 let settings = make_settings ( ) ;
0 commit comments