Choose your stack
Fix clickjacking in Node.js and Express
Send a framing policy on sensitive pages. Content-Security-Policy: frame-ancestors 'none' (or 'self' and named partners) is what modern browsers honor; add X-Frame-Options: DENY for old ones.
Open the fix card Python · FlaskFix clickjacking in Python and Flask
Refuse framing in an HTTP response header, not in the page. Send Content-Security-Policy: frame-ancestors 'none' (which modern browsers honor over X-Frame-Options) plus X-Frame-Options: DENY for older browsers, on every page except one you mean to be embedded.
Open the fix card TypeScript · Next.jsFix clickjacking in Next.js
Send a framing policy on sensitive pages, in middleware so every matched response carries it.
Open the fix card Python · DjangoFix clickjacking in Python and Django
Keep Django's XFrameOptionsMiddleware (X-Frame-Options defaults to DENY) and add a CSP with ContentSecurityPolicyMiddleware and SECURE_CSP = {"frame-ancestors": [CSP.NONE]}, which browsers honor over X-Frame-Options.
Open the fix cardStacks without a card
- Java and Spring Boot: no card yet. The nearest is Fix clickjacking in Node.js and Express.
- C# and ASP.NET Core: no card yet. The nearest is Fix clickjacking in Node.js and Express.
- Python and FastAPI: no card yet. The nearest is Fix clickjacking in Python and Flask, in the same language.
- Go and net/http: no card yet. The nearest is Fix clickjacking in Node.js and Express.
- Ruby on Rails: no card yet. The nearest is Fix clickjacking in Python and Django.
- PHP and Laravel: no card yet. The nearest is Fix clickjacking in Python and Django.
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
Practice in the browser
- CSP builder lab: Build a strict Content-Security-Policy · Practitioner, JavaScript
Guides on this weakness
Configure CORS and CSP around the browser capabilities you need
Control who can read API responses and which resources a page may execute without confusing either control with authorization.
Configure HTTP security headers from a tested baseline
Send the response headers that make browsers enforce HTTPS, run only your own scripts, refuse hostile framing and keep cookies on one host, then prove they appear on every kind of response.