Skip to content

feat(ads): let a video unit measure itself, and harden the field that made that risky - #323

Merged
ralyodio merged 1 commit into
masterfrom
ads-sandbox-scripts-and-font-hardening
Sep 25, 2026
Merged

ralyodio merged 1 commit into
masterfrom
ads-sandbox-scripts-and-font-hardening

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Anthony asked for both halves of the open question from #320: harden fontFamily, then grant the sandbox.

The sandbox

ad.js framed every creative with no allow-scripts, so an in-banner video served through the JSON tag could not report anything — roughly 9 video impressions a day, chosen and rendered and silent. That permission is now granted, for video and audio fills only, and the serve route injects the same beacon the frame path already uses.

Per-medium rather than blanket, following the autoplay permission immediately below it in the same file: a static banner has nothing to report and still runs nothing at all.

allow-same-origin is not granted and must never be. The two together let a framed document reach frameElement and delete its own sandbox attribute, which is not a sandbox. Without it the creative keeps an opaque origin: it cannot read the publisher's DOM, cookies or storage, and the worst a compromised creative gets is its own inert box. There is a test asserting the string never appears in a sandbox value (comments stripped, since the tag explains the rule in prose).

The hardening, which is the precondition

font_family was the one advertiser-derived value reaching a CSS context, interpolated raw in four places. esc() is the wrong tool there — inside a <style> block &quot; is not a quote and } is still a closing brace — so an unfiltered value could close the rule and open its own. Harmless while nothing executes; not something to leave standing while granting scripts.

safeFontFamily allow-lists the shape rather than escaping it: letters, digits, spaces, commas, hyphens, underscores. That covers both stacks in production across 2,978 creatives (system-ui, -apple-system, Segoe UI, Roboto, sans-serif and system-ui, sans-serif). Quotes are refused outright — Helvetica Neue is valid CSS unquoted, so the quoted form buys nothing and costs the character most useful for breaking out.

Applied where the value is interpolated, not only where it is saved, so it is true for every row already stored rather than only for new ones. The save path is narrowed too.

Worth noting for scope: font_family is not settable through the campaign API today, and no stored value contains a bracket. This closes the hole before it matters, rather than after.

Verification

  • npm run typecheck clean
  • npx vitest run — 2,696 passed, 2 skipped, 0 failed (10 new across two files)
  • next build --webpack compiles
  • No migration

🤖 Generated with Claude Code

… made that risky

Adds allow-scripts to the ad.js srcdoc iframe for video and audio fills
only, so an in-banner video served through the JSON tag can report
playback the way the frame path already does. Roughly 9 video
impressions a day were invisible: chosen, rendered, and silent.

allow-same-origin is NOT granted and must never be. The two together let
a framed document reach frameElement and delete its own sandbox
attribute, which is not a sandbox. Without it the creative keeps an
opaque origin: it cannot read the publisher's DOM, cookies or storage,
and a compromised creative gets its own inert box and nothing else.
There is a test asserting the string never appears in a sandbox value.

Granting it per-medium rather than to every fill follows the autoplay
permission immediately below it: a static banner has nothing to report
and still runs nothing at all.

The hardening is the precondition. `font_family` was the one
advertiser-derived value reaching a CSS context, interpolated raw in
four places. esc() is the wrong tool there — inside a <style> block
`&quot;` is not a quote and `}` is still a closing brace — so an
unfiltered value could close the rule and open its own. Harmless while
nothing executes; not something to leave standing while granting
scripts.

safeFontFamily allow-lists the shape instead of escaping: letters,
digits, spaces, commas, hyphens, underscores, which covers both stacks
in production across 2,978 creatives. Applied where the value is
interpolated, not only where it is saved, so it is true for every row
already stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

49 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 32 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-predictable-temp-path ops/selfhost/server/setup-supabase.sh:201
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit eda32b7 into master Sep 25, 2026
10 checks passed
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.

1 participant