Skip to content

Comments

Add GAM interceptor integration#349

Draft
aram356 wants to merge 2 commits intomainfrom
feature/gam-integration
Draft

Add GAM interceptor integration#349
aram356 wants to merge 2 commits intomainfrom
feature/gam-integration

Conversation

@aram356
Copy link
Collaborator

@aram356 aram356 commented Feb 20, 2026

Summary

Combines #240 (backend) and #241 (frontend) into a single PR.

  • Backend (Rust): Adds GamIntegrationConfig with enabled, bidders, and force_render options. Implements IntegrationHeadInjector to inject GAM config script (window.tsGamConfig) into <head>. Registers GAM integration in the integration builder system.
  • Frontend (TypeScript): Adds client-side GAM interceptor that intercepts GPT slotRenderEnded events and renders Prebid creatives when GAM doesn't have matching line items. Supports multiple rendering methods (iframe src replacement, doc.write, iframe.src, pbjs.renderAd fallback). Forwards GAM config from core config to GAM integration via lazy loader.

Configuration

Server-side (TOML):

[integrations.gam]
enabled = true
bidders = ["mocktioneer"]
force_render = false

Client-side (JS):

tsjs.setConfig({
  gam: {
    enabled: true,
    bidders: ['mocktioneer'],
    forceRender: false
  }
});

Test plan

  • npx vitest run passes (162 tests across 17 files)
  • cargo test -p trusted-server-common -- gam passes (3 tests)
  • Test GAM interceptor with a real GAM/Prebid setup

Closes #248
Closes #249
Supersedes #240 and #241
Related to #179

- Add `GamIntegrationConfig` with enabled, bidders, and force_render options
- Implement `IntegrationHeadInjector` to inject GAM config script into <head>
- Register GAM integration in the integration builder system
- Add unit tests for config script generation

Configuration example:
```toml
[integrations.gam]
enabled = true
bidders = ["mocktioneer"]  # Only intercept these bidders, empty = all
force_render = false       # Force render even if GAM has a line item
```

The injected script sets `window.tsGamConfig` which is picked up by the
client-side GAM interceptor on initialization.
- Add client-side GAM interceptor that intercepts GPT slotRenderEnded events
- Render Prebid creatives when GAM doesn't have matching line items
- Support multiple rendering methods: iframe src replacement, doc.write, pbjs.renderAd
- Add GamConfig interface for configuration via tsjs.setConfig({ gam: {...} })
- Forward GAM config from core config to GAM integration via lazy loader
- Add comprehensive tests for iframe attribute extraction and interceptor behavior

The GAM interceptor supports:
- Filtering by specific bidders (bidders option)
- Force rendering even when GAM has a line item (forceRender option)
- Auto-initialization on module load
- Stats tracking for debugging
@aram356 aram356 changed the title Add GAM interceptor integration (backend + frontend) Add GAM interceptor integration Feb 20, 2026

// Verify this is mostly just an iframe (no complex content after)
// Allow trailing whitespace, newlines, and closing tag
const afterIframe = trimmed.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/i, '').trim();

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High

This string may still contain
<iframe
, which may cause an HTML element injection vulnerability.

Copilot Autofix

AI 5 days ago

In general, to fix incomplete multi‑character sanitization, either (a) apply the replacement repeatedly until it no longer changes the string, or (b) avoid complex multi-character patterns when validating and instead use a simpler, more robust approach. Here, we do not actually need to “sanitize” the whole HTML; we only need to verify that the markup is essentially just a single iframe wrapper (no significant extra content). So we can improve the validation so that it cannot be bypassed by having remaining <iframe text after a single replacement.

The best targeted fix here is to adjust how afterIframe is computed. Currently it does a single replace of one <iframe...>...</iframe> pair. Instead, we can strip all iframe blocks and then check whether anything non-whitespace remains. We can do that by using the same regex with the global (g) modifier and looping until no further changes are made (safe and explicit), or by using a simpler loop. This keeps the external behavior the same (we still accept only creatives that are “mostly just an iframe”) but closes the incomplete sanitization gap. The change is entirely within extractIframeAttrs in crates/js/lib/src/integrations/gam/index.ts, around the existing afterIframe logic. No new imports or exports are required.

Concretely: replace the single-call trimmed.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/i, '').trim(); with a small loop that repeatedly removes all occurrences of <iframe...>...</iframe> until the string stops changing, then trims and checks the result as before. This guarantees that if any <iframe constructs are present, they are all removed before we perform the “significant content” test, eliminating the incomplete multi-character replacement.

Suggested changeset 1
crates/js/lib/src/integrations/gam/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/js/lib/src/integrations/gam/index.ts b/crates/js/lib/src/integrations/gam/index.ts
--- a/crates/js/lib/src/integrations/gam/index.ts
+++ b/crates/js/lib/src/integrations/gam/index.ts
@@ -104,7 +104,14 @@
 
   // Verify this is mostly just an iframe (no complex content after)
   // Allow trailing whitespace, newlines, and closing tag
-  const afterIframe = trimmed.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/i, '').trim();
+  let afterIframe = trimmed;
+  const iframePattern = /<iframe[^>]*>[\s\S]*?<\/iframe>/gi;
+  let previous: string;
+  do {
+    previous = afterIframe;
+    afterIframe = afterIframe.replace(iframePattern, '');
+  } while (afterIframe !== previous);
+  afterIframe = afterIframe.trim();
   if (afterIframe.length > 0 && !afterIframe.match(/^[\s\n]*$/)) {
     // Has significant content after iframe, not a simple wrapper
     return null;
EOF
@@ -104,7 +104,14 @@

// Verify this is mostly just an iframe (no complex content after)
// Allow trailing whitespace, newlines, and closing tag
const afterIframe = trimmed.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/i, '').trim();
let afterIframe = trimmed;
const iframePattern = /<iframe[^>]*>[\s\S]*?<\/iframe>/gi;
let previous: string;
do {
previous = afterIframe;
afterIframe = afterIframe.replace(iframePattern, '');
} while (afterIframe !== previous);
afterIframe = afterIframe.trim();
if (afterIframe.length > 0 && !afterIframe.match(/^[\s\n]*$/)) {
// Has significant content after iframe, not a simple wrapper
return null;
Copilot is powered by AI and may make mistakes. Always verify output.
@aram356 aram356 self-assigned this Feb 20, 2026
@aram356 aram356 marked this pull request as draft February 20, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GAM interceptor integration - frontend Add GAM interceptor integration - backend

1 participant