Choose your stack

Stacks without a card

How it happens

  • A sensitive page sends no framing policy, so any site can load it in an invisible iframe, overlay a decoy interface, and trick a victim into clicking the real control (a confirm, a delete, a transfer).
  • The page relies on X-Frame-Options: ALLOW-FROM to restrict framing to a partner, but Chromium never supported ALLOW-FROM and Firefox removed it, so the header is ignored and the page frames anywhere.
  • A Content-Security-Policy is present but has no frame-ancestors directive; default-src does not cover framing, so there is no restriction.
  • Protection is a frame-busting script (if top != self ...), which a sandboxed iframe and navigation-cancel tricks defeat.
  • The header is set on the main page but not on an embeddable sub-resource or an error page, or a middleware matcher misses the route, leaving a gap.

Fix principles

  • Send Content-Security-Policy: frame-ancestors 'none' on pages that must not be framed, or frame-ancestors 'self' https://partner.example for pages you or a named partner frames.
  • Add X-Frame-Options: DENY (or SAMEORIGIN) as a fallback for browsers too old for frame-ancestors; CSP wins where both are understood. Send one coherent policy, not two conflicting values.
  • Set the headers where every response carries them -- middleware, a security-headers filter, or the framework's headers configuration -- so a new route under the protected path is covered by default.
  • Remove X-Frame-Options: ALLOW-FROM and any frame-busting script; neither protects a modern browser.
  • Remember frame-ancestors governs who may frame you, not whether you frame others, and that it is defense for UI redress, not a substitute for CSRF tokens on the actions those pages perform.

The scenario

A sensitive, state-changing page (confirm a payout) is served by the app. The fixed build sends Content-Security-Policy: frame-ancestors 'none' (and X-Frame-Options: DENY) so no other origin may frame it; the vulnerable build sends no framing headers; the wrong build sends only X-Frame-Options: ALLOW-FROM, which modern browsers ignore. The tests read the response headers, which is what the browser acts on.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The exploit tests read the response headers and assert a restrictive frame-ancestors is present (not * and not an http/https wildcard); X-Frame-Options: DENY or SAMEORIGIN counts as a fallback, ALLOW-FROM does not. The functional tests confirm the page is still served.
  • In a browser, build a page on another origin with an <iframe> pointing at the sensitive URL: with the fix the frame is blank and the console shows the frame-ancestors refusal; without it the page renders in the frame.
  • Do not count on static analysis: no rule flags a response that omits a framing policy or one that uses the obsolete ALLOW-FROM. Each card's scannerRules records what was checked.

Identifiers

CWE-1021 (Improper Restriction of Rendered UI Layers or Frames) is the weakness the cards show: a sensitive page can be framed by another site. It maps to OWASP Top 10:2025 A06 Insecure Design (A06's official CWE list includes CWE-1021). CAPEC-103 (Clickjacking) and CAPEC-222 (iFrame Overlay) are the attack patterns MITRE links to CWE-1021. The 2025 CWE Top 25 does not rank it.

Attack patterns:

References

  1. OWASP Top 10:2025 A06 Insecure Design
  2. CWE-1021: Improper Restriction of Rendered UI Layers or Frames
  3. OWASP Clickjacking Defense Cheat Sheet
  4. MDN: CSP frame-ancestors
  5. MDN: X-Frame-Options (ALLOW-FROM is non-standard and obsolete)

Practice in the browser

Guides on this weakness