Creative Processing
Trusted Server rewrites ad creative HTML and CSS to route resources through first-party domains.
Overview
Creative processing has separate auction-response and proxied-response paths. When rewriting is enabled for a path, it rewrites URLs in third-party ad creatives so resources are fetched through the publisher's first-party domain. This provides:
- First-party delivery: eligible, non-excluded external resource URLs route through the publisher's domain
- First-party context: cookies and storage use the publisher's domain
- EC ID integration: configurable ID forwarding to downstream endpoints
- Tamper resistance: validated, signed URLs prevent in-flight URL substitution
- Configurable data sharing: forwarding to vendors is per the deployer's configuration
How It Works
┌──────────────────────────────────────────────────────┐
│ Original Creative HTML │
│ <img src="https://tracker.com/pixel.gif"> │
│ <iframe src="https://cdn.com/ad.html"> │
│ <style> .bg { background: url(cdn.com/bg.jpg); } │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Trusted Server Processing (rewrite-enabled) │
│ 1. Sanitize POST /auction adm when opted in │
│ 2. Parse HTML with streaming processor │
│ 3. Detect absolute/protocol-relative URLs │
│ 4. Generate signed proxy URLs │
│ 5. Rewrite in-place and inject TSJS │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Rewritten Creative HTML │
│ <img src="/first-party/proxy?tsurl=...&tstoken=sig">│
│ <iframe src="/first-party/proxy?tsurl=...&token=sig">│
│ <style> .bg { background: url(/first-party/proxy...) }│
└──────────────────────────────────────────────────────┘Processing Triggers
The creative rewriters are invoked by independent delivery paths:
- Auction
adm: Winning-bid HTML returned byPOST /auctionis optionally sanitized ([auction].sanitize_creatives, opt-in, defaultfalse) and rewritten ([auction].rewrite_creatives, defaulttrue). - First-party proxy: Non-streaming
text/htmlandtext/cssresponses fetched through/first-party/proxyare rewritten independently of the auction setting. - Integration processing: Publisher HTML integrations use their own registration and configuration controls.
Streaming Mode
When with_streaming() is enabled in ProxyRequestConfig, proxied HTML/CSS processing is skipped to preserve origin compression and reduce latency. This does not change the POST /auction sanitizer or rewrite setting.
Auction Rewrite Control
Two independent auction settings control the processing applied to winning-bid adm returned by POST /auction and delivered through the publisher SSAT/page-bids path. Sanitization is opt-in (default false); rewriting is enabled by default:
[auction]
sanitize_creatives = false
rewrite_creatives = trueRegardless of mode, a creative larger than the 1 MiB per-creative cap is rejected and its adm is dropped.
sanitize_creatives | rewrite_creatives | Auction winning-bid adm behavior |
|---|---|---|
false (default) | false | Deliver the creative exactly as the bidder returned it (subject to the size cap). |
true | false | Strip executable markup (script/object/embed/form, event handlers) with its inner content, then deliver without rewriting. Sanitizer-accepted external resource, click, and inline CSS URLs remain direct. |
false | true (default) | Rewrite eligible resource/CSS and click URLs in the raw bidder markup to signed first-party endpoints, removing any bidder <base> element. Executable markup is preserved. |
true | true | Sanitize first, then rewrite. POST /auction emits root-relative endpoints and injects creative TSJS exactly once, whether or not the bidder supplied a <body>; SSAT/page-bids emits absolute endpoints for its foreign-origin renderer and does not inject the bundle. |
Sanitization blanks script-based creatives
Sanitization removes script/object/embed/form and similar elements together with their inner content, which destroys script-based creatives — the majority of programmatic display. Enable sanitize_creatives when creatives render in a context that shares the publisher's origin; leave it disabled when creatives render in a foreign-origin frame (for example the Prebid Universal Creative inside the ad server's iframe), where the markup cannot reach the publisher origin. Sanitizer acceptance is not a host allowlist; ordinary accepted HTTP(S) URLs may cause the browser to contact external creative hosts directly.
Runtime protections inside the sandboxed creative iframe
Creatives rendered by Trusted Server's own path run in a sandboxed iframe without allow-same-origin, i.e. an opaque origin. The injected creative runtime's click guard recovers mutated clicks there via a GET /first-party/proxy-rebuild navigation (302 chain).
Two capabilities are unavailable in that context. CORS-mode subresources — ES modules, crossorigin fonts, fetch/XHR — cannot load through /first-party/proxy, because that endpoint deliberately sends no Access-Control-Allow-Origin: it is a generic signed fetcher that forwards the EC ID and client-derived headers, so letting an opaque creative frame read its responses would turn it into a readable bidder-controlled proxy. Ordinary subresources (<img>, <script src>, stylesheets, media) are unaffected. A separately constrained asset capability is tracked in #982.
The second is dynamic resource signing, which rewrites URLs on elements a creative inserts at runtime. It is installed only when renderGuard is enabled in tsCreativeConfig, and that is false by default — deployments using the default configuration are unaffected. Where it is enabled, runtime-inserted <img>/<iframe> URLs cannot be signed from an opaque origin (the signing request is blocked by CORS), so they load directly from third parties; the sandbox still isolates them from the publisher origin, but they are not first-party proxied. A same-origin parent postMessage broker restoring dynamic signing is tracked in #982. URLs rewritten server-side are unaffected.
PBS Cache coordinates and processing
On the publisher SSAT/page-bids path, hb_cache_host/hb_cache_path let the client fetch and render the cached bid, whose adm is the bidder's original markup and therefore bypasses every server-side processing policy. They are emitted only for bids that supplied no creative of their own, where the cache is the sole render source. A bid that supplied a creative — whether processing accepted it, or rejected it as empty, unparseable, or over the size limit — ships without them, so a processed or refused creative can never be re-fetched raw.
Neither setting affects HTML/CSS response rewriting under /first-party/proxy. The publisher SSAT/page-bids path is a production creative path and follows both settings. [debug].inject_adm_for_testing controls only the additional debug_bid diagnostic blob and testing-only direct GAM replacement; it does not control whether the processed adm is present.
Rewritten Elements
Images
Elements: <img>, <input type="image">
Attributes:
src- Primary image sourcedata-src- Lazy-loading sourcesrcset- Responsive image sourcesimagesrcset- Image set (used in<link>)
Example:
<!-- Original -->
<img
src="https://cdn.example.com/banner.jpg"
srcset="
https://cdn.example.com/banner@1x.jpg 1x,
https://cdn.example.com/banner@2x.jpg 2x
"
/>
<!-- Rewritten -->
<img
src="/first-party/proxy?tsurl=https://cdn.example.com/banner.jpg&tstoken=sig1"
srcset="
/first-party/proxy?tsurl=https://cdn.example.com/banner@1x.jpg&tstoken=sig2 1x,
/first-party/proxy?tsurl=https://cdn.example.com/banner@2x.jpg&tstoken=sig3 2x
"
/>Scripts
Elements: <script>
Attributes:
src- Script source URL
Example:
<!-- Original -->
<script src="https://cdn.example.com/tracker.js"></script>
<!-- Rewritten -->
<script src="/first-party/proxy?tsurl=https://cdn.example.com/tracker.js&tstoken=sig"></script>Media Elements
Elements: <video>, <audio>, <source>
Attributes:
src- Media source URL
Example:
<!-- Original -->
<video>
<source src="https://cdn.example.com/video.mp4" type="video/mp4" />
</video>
<!-- Rewritten -->
<video>
<source
src="/first-party/proxy?tsurl=https://cdn.example.com/video.mp4&tstoken=sig"
type="video/mp4"
/>
</video>Embedded Objects
Elements: <object>, <embed>
Attributes:
data- Object data source (<object>)src- Embed source (<embed>)
Example:
<!-- Original -->
<object data="https://example.com/flash.swf"></object>
<embed src="https://example.com/media.swf" />
<!-- Rewritten -->
<object
data="/first-party/proxy?tsurl=https://example.com/flash.swf&tstoken=sig"
></object>
<embed
src="/first-party/proxy?tsurl=https://example.com/media.swf&tstoken=sig"
/>Iframes
Elements: <iframe>
Attributes:
src- Frame source URL
Example:
<!-- Original -->
<iframe src="https://advertiser.com/creative.html"></iframe>
<!-- Rewritten -->
<iframe
src="/first-party/proxy?tsurl=https://advertiser.com/creative.html&tstoken=sig"
></iframe>Nested Iframes
If the iframe content itself contains HTML, it will be processed recursively. Each level of nesting gets its own URL rewriting pass.
Links
Elements: <link>
Attributes:
href- Link target (stylesheets, preload, prefetch)imagesrcset- Responsive images in link preload
Conditions: Only rewritten when rel attribute matches:
stylesheetpreloadprefetch
Example:
<!-- Original -->
<link rel="stylesheet" href="https://cdn.example.com/styles.css" />
<link rel="preload" href="https://cdn.example.com/font.woff2" as="font" />
<!-- Rewritten -->
<link
rel="stylesheet"
href="/first-party/proxy?tsurl=https://cdn.example.com/styles.css&tstoken=sig"
/>
<link
rel="preload"
href="/first-party/proxy?tsurl=https://cdn.example.com/font.woff2&tstoken=sig"
as="font"
/>Anchors (Click Tracking)
Elements: <a>, <area>
Attributes:
href- Link destination
Rewrite Mode: Uses /first-party/click for direct redirects
Example:
<!-- Original -->
<a href="https://advertiser.com/product?id=123">Buy Now</a>
<!-- Rewritten -->
<a
href="/first-party/click?tsurl=https://advertiser.com/product&id=123&tstoken=sig"
>Buy Now</a
>Click vs Proxy
Anchors (<a>) use /first-party/click for 302 redirects, avoiding content downloads. Other elements use /first-party/proxy to fetch and potentially rewrite content.
SVG Elements
Elements: SVG <image>, <use>
Attributes:
href- SVG 2.0 syntaxxlink:href- SVG 1.1 legacy syntax
Example:
<!-- Original -->
<svg>
<image href="https://cdn.example.com/icon.svg" />
<use xlink:href="https://cdn.example.com/sprite.svg#icon" />
</svg>
<!-- Rewritten -->
<svg>
<image
href="/first-party/proxy?tsurl=https://cdn.example.com/icon.svg&tstoken=sig"
/>
<use
xlink:href="/first-party/proxy?tsurl=https://cdn.example.com/sprite.svg&tstoken=sig#icon"
/>
</svg>Inline Styles
Attributes: style attribute on any element
Patterns: Rewrites url(...) values in CSS
Example:
<!-- Original -->
<div style="background: url(https://cdn.example.com/bg.png) no-repeat;">
<!-- Rewritten -->
<div
style="background: url(/first-party/proxy?tsurl=https://cdn.example.com/bg.png&tstoken=sig) no-repeat;"
></div>
</div>Style Blocks
Elements: <style>
Patterns: Rewrites all url(...) occurrences in CSS
Example:
<!-- Original -->
<style>
.header {
background-image: url(https://cdn.example.com/header.jpg);
}
.logo {
background: url('https://cdn.example.com/logo.png');
}
</style>
<!-- Rewritten -->
<style>
.header {
background-image: url(/first-party/proxy?tsurl=https://cdn.example.com/header.jpg&tstoken=sig);
}
.logo {
background: url('/first-party/proxy?tsurl=https://cdn.example.com/logo.png&tstoken=sig');
}
</style>URL Detection Rules
Absolute URLs
Pattern: Starts with http:// or https://
Rewritten: ✅ Yes
Examples:
✅ https://cdn.example.com/image.png
✅ http://tracker.example.com/pixel.gif
❌ /relative/path.jpg (relative)
❌ ../images/logo.png (relative)Protocol-Relative URLs
Pattern: Starts with //
Rewritten: ✅ Yes (normalized to https://)
Examples:
Original: //cdn.example.com/script.js
Normalized: https://cdn.example.com/script.js
Rewritten: /first-party/proxy?tsurl=https://cdn.example.com/script.js&tstoken=sigRelative URLs
Patterns:
- Starts with
/(absolute path) - Starts with
./or../(relative path) - No scheme prefix (relative)
Rewritten: ❌ No
Examples:
❌ /assets/image.png
❌ ./local.jpg
❌ ../parent/file.css
❌ image.pngWhy Skip Relative URLs?
Relative URLs already point to your domain (publisher origin). Rewriting them would create unnecessary proxy loops and break functionality.
Non-Network Schemes
Skipped Schemes:
data:- Data URIs (inline content)javascript:- JavaScript executionmailto:- Email linkstel:- Phone numbersblob:- Blob objectsabout:- Browser internal pages
Rewritten: ❌ No
Examples:
❌ data:image/png;base64,iVBORw0KGgo...
❌ javascript:void(0)
❌ mailto:contact@example.com
❌ tel:+1234567890
❌ blob:https://example.com/uuid
❌ about:blankSrcset Processing
Srcset Syntax
Srcset attributes contain comma-separated candidates with optional descriptors:
Format: url descriptor, url descriptor, ...
Descriptors:
1x,2x,3x- Pixel density100w,200w- Width in pixels
Parsing Rules
Robust Comma Handling:
- Splits on commas with or without spaces
- Preserves
data:URIs (doesn't split on internal commas) - Handles irregular spacing
Example:
<!-- Various spacing patterns -->
srcset="url1.jpg 1x, url2.jpg 2x"
<!-- standard -->
srcset="url1.jpg 1x,url2.jpg 2x"
<!-- no space after comma -->
srcset="url1.jpg 1x , url2.jpg 2x"
<!-- extra spaces -->Descriptor Preservation
Descriptors are preserved exactly as written:
<!-- Original -->
<img
srcset="
https://cdn.com/small.jpg 480w,
https://cdn.com/medium.jpg 800w,
https://cdn.com/large.jpg 1200w
"
/>
<!-- Rewritten -->
<img
srcset="
/first-party/proxy?tsurl=https://cdn.com/small.jpg&tstoken=sig1 480w,
/first-party/proxy?tsurl=https://cdn.com/medium.jpg&tstoken=sig2 800w,
/first-party/proxy?tsurl=https://cdn.com/large.jpg&tstoken=sig3 1200w
"
/>Mixed URL Types
Srcset can mix absolute and relative URLs:
<!-- Original -->
<img srcset="/local/small.jpg 1x, https://cdn.com/large.jpg 2x" />
<!-- Rewritten (only absolute URL) -->
<img
srcset="
/local/small.jpg 1x,
/first-party/proxy?tsurl=https://cdn.com/large.jpg&tstoken=sig 2x
"
/>CSS URL Rewriting
url() Syntax Variations
CSS url() values support multiple quote styles:
/* No quotes */
url(https://example.com/image.png)
/* Single quotes */
url('https://example.com/image.png')
/* Double quotes */
url("https://example.com/image.png")
/* With spaces */
url( "https://example.com/image.png" )All are rewritten correctly, preserving the original quote style.
CSS Properties
Common properties with url() values:
/* Background images */
background: url(https://cdn.com/bg.jpg);
background-image: url(https://cdn.com/pattern.png);
/* Borders */
border-image: url(https://cdn.com/border.svg);
/* List styles */
list-style-image: url(https://cdn.com/bullet.png);
/* Cursors */
cursor: url(https://cdn.com/cursor.cur), pointer;
/* Masks */
mask-image: url(https://cdn.com/mask.svg);
/* Filters */
filter: url(https://cdn.com/filter.svg#blur);All url() occurrences are rewritten regardless of property.
Multiple url() Values
Properties can have multiple url() values:
/* Original */
.element {
background:
url(https://cdn.com/top.png) top,
url(https://cdn.com/bottom.png) bottom;
}
/* Rewritten */
.element {
background:
url(/first-party/proxy?tsurl=https://cdn.com/top.png&tstoken=sig1) top,
url(/first-party/proxy?tsurl=https://cdn.com/bottom.png&tstoken=sig2) bottom;
}@import Rules
CSS @import with URLs:
/* Original */
@import url(https://fonts.googleapis.com/css?family=Roboto);
/* Rewritten */
@import url(/first-party/proxy?tsurl=https://fonts.googleapis.com/css?family=Roboto&tstoken=sig);Exclude Domains
Configuration
Prevent specific domains from being rewritten:
[rewrite]
exclude_domains = [
"*.cdn.trusted-partner.com", # Wildcard pattern
"first-party.example.com", # Exact match
"localhost", # Development
]Pattern Matching
Wildcard Patterns: * matches any subdomain
Pattern: *.cdn.example.com
Matches:
✅ assets.cdn.example.com
✅ images.cdn.example.com
❌ cdn.example.com (no subdomain)
❌ cdn.example.com.evil.com (different domain)Exact Patterns: No * requires exact host match
Pattern: api.example.com
Matches:
✅ api.example.com
❌ www.api.example.com
❌ api.example.com.evil.comUse Cases
Trusted Partners:
exclude_domains = ["*.trusted-cdn.com"]Skip rewriting for partners already providing first-party scripts.
Development:
exclude_domains = ["localhost", "127.0.0.1"]Avoid proxying local development servers.
Same-Origin Resources:
exclude_domains = ["assets.publisher.com"]Skip resources already on your domain.
Integration Hooks
Attribute Rewriters
Integrations can override attribute rewriting:
Example: Next.js integration rewrites origin URLs
impl IntegrationAttributeRewriter for NextJsIntegration {
fn rewrite(&self, attr_name: &str, attr_value: &str, ctx: &Context)
-> AttributeRewriteAction
{
if attr_name == "href" && attr_value.contains(&ctx.origin_host) {
let rewritten = attr_value.replace(&ctx.origin_host, &ctx.request_host);
return AttributeRewriteAction::replace(rewritten);
}
AttributeRewriteAction::keep()
}
}Actions:
keep()- Leave attribute unchangedreplace(value)- Change attribute valueremove_element()- Delete entire element
Script Rewriters
Integrations can modify <script> content:
Example: Next.js rewrites __NEXT_DATA__ JSON
impl IntegrationScriptRewriter for NextJsIntegration {
fn selector(&self) -> &'static str {
"script#__NEXT_DATA__"
}
fn rewrite(&self, content: &str, ctx: &Context) -> ScriptRewriteAction {
let rewritten = rewrite_next_data_urls(content, ctx);
ScriptRewriteAction::replace(rewritten)
}
}Actions:
keep()- Leave script unchangedreplace(content)- Replace script contentremove_node()- Delete script element
Head Injectors
Integrations can inject HTML snippets at the start of <head>, immediately after the unified TSJS bundle:
Example: An integration injects configuration that runs after the TSJS API is available
impl IntegrationHeadInjector for MyIntegration {
fn integration_id(&self) -> &'static str { "my_integration" }
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
vec![format!(
r#"<script>tsjs.setConfig({{ host: "{}" }});</script>"#,
ctx.request_host
)]
}
}Behavior:
- Snippets are prepended into
<head>after the TSJS bundle tag - Called once per HTML response
- Multiple integrations can each contribute snippets
- If no snippets are returned, no extra markup is added
See Integration Guide for creating custom rewriters.
TSJS Injection
Automatic Injection
The Trusted Server JavaScript (TSJS) library is automatically injected:
Location: Start of <head> element
Tag:
<script
async
src="/static/tsjs-core.min.js"
data-tsjs-integration="core"
></script>Timing: Injected once per HTML response before any other scripts.
Integration Bundles
Integrations can request additional bundles:
IntegrationRegistration::builder("my_integration")
.with_asset("my_integration") // Requests tsjs-my_integration.min.js
.build()Result:
<head>
<script
async
src="/static/tsjs-core.min.js"
data-tsjs-integration="core"
></script>
<script
async
src="/static/tsjs-my_integration.min.js"
data-tsjs-integration="my_integration"
></script>
<!-- Rest of head content -->
</head>Bundle Types
Each integration is built as a separate IIFE at compile time (crates/trusted-server-js/dist/):
tsjs-core.js— Core API (always included)tsjs-creative.js— Creative click-guard and tracking- Prebid is built externally with
build-prebid-external.mjsand served through/integrations/prebid/bundle.js tsjs-lockr.js,tsjs-permutive.js,tsjs-didomi.js,tsjs-datadome.js,tsjs-testlight.js— Other integrations
At runtime, the server concatenates tsjs-core.js + the modules for enabled integrations. The URL stays /static/tsjs=tsjs-unified.min.js?v=<hash> for backward compatibility.
Performance Optimization
Streaming Processing
HTML is processed in chunks (default 8192 bytes):
Benefits:
- Low memory footprint
- Handles large creatives
- Incremental output
- Fast first byte
Trade-offs:
- Cannot access full DOM
- Element-by-element processing
- No look-ahead
Compression
Buffered Mode (with rewriting):
Origin Response (gzipped)
↓ Decompress
Processing
↓ Rewrite URLs
Response (uncompressed)
↓ Fastly edge can re-compress
ClientStreaming Mode (no rewriting):
Origin Response (gzipped)
↓ Passthrough
Client (stays gzipped)Use streaming for binary/large files to preserve compression.
Caching
Rewritten creatives can be cached:
Cache Key Components:
- Original creative URL
- Publisher domain
- Integration configuration
Headers to Set:
Cache-Control: public, max-age=3600
Vary: Accept-EncodingDebugging
Logging
Enable debug logging for rewrite operations:
log::debug!("creative: rewriting {} -> {}", original_url, proxy_url);
log::debug!("creative: excluded domain {}", url);
log::debug!("creative: skipped non-network scheme {}", url);Testing Rewrites
Manual Testing:
- Save original creative HTML to file
- Pass through rewrite function
- Compare output
let original = "<img src=\"https://tracker.com/pixel.gif\">";
let rewritten = rewrite_creative_html(&settings, original);
assert!(rewritten.contains("/first-party/proxy"));Integration Tests:
#[test]
fn test_image_src_rewrite() {
let html = r#"<img src="https://cdn.example.com/banner.jpg">"#;
let result = rewrite_creative_html(&test_settings(), html);
assert!(result.contains("/first-party/proxy?tsurl="));
assert!(result.contains("&tstoken="));
}Common Issues
Relative URLs Not Working:
- Ensure origin response includes proper
<base>tag - Or convert to absolute URLs before rewriting
Data URIs Being Rewritten:
- Should be automatically skipped
- Check for malformed
data:scheme
Srcset Parsing Errors:
- Verify comma-separated format
- Check for unclosed quotes
Security Considerations
URL Validation
All rewritten URLs are validated:
- Scheme Check: Only
http://andhttps:// - Signature: HMAC-SHA256 token required
- Expiration: Optional
tsexptimestamp - Exclusion List: Configurable domain blacklist
Content Security Policy
Recommended CSP headers for rewritten creatives:
Content-Security-Policy:
default-src 'self';
img-src 'self' /first-party/proxy;
script-src 'self' /static/tsjs;
style-src 'self' 'unsafe-inline';
frame-src 'self' /first-party/proxy;Injection Prevention
Automatic Protection:
- URLs are properly encoded
- No raw user input in rewrites
- Signature prevents tampering
Manual Checks:
- Validate origin creative sources
- Sanitize user-generated content
- Monitor for suspicious patterns
Best Practices
Configuration
✅ Do:
- Use strong
proxy_secret(32+ bytes random) - Exclude trusted first-party domains
- Set appropriate cache headers
- Test rewrites before production
❌ Don't:
- Hardcode secrets in source
- Rewrite same-origin URLs unnecessarily
- Skip signature validation
- Disable TSJS injection without reason
Performance
✅ Do:
- Use streaming for large/binary responses
- Enable compression at edge
- Cache rewritten creatives
- Monitor rewrite latency
❌ Don't:
- Buffer entire response unnecessarily
- Rewrite on every request (cache!)
- Process non-HTML/CSS with rewriter
- Chain multiple rewrites
Monitoring
Track these metrics:
- Rewrite operations - Count of rewrites per request
- Excluded domains - Frequency of exclusions
- Processing time - Latency added by rewriting
- Cache hit rate - Effectiveness of caching
- TSJS injection - Verify library loads
Next Steps
- Learn about First-Party Proxy for URL handling
- Review Integration Guide for custom rewriters
- Set up Configuration for your creatives
- Explore Edge Cookies for identity management