Missing Anti-Clickjacking Header? What It Means and How to Fix It
Published July 2026 · sourced from MDN and the CSP specification
You ran a vulnerability scanner. The report came back with something like this:
Scanner findings you might be seeing:
- ZAP / StackHawk: “Missing Anti-clickjacking Header” (Alert 10020-1)
- Nessus / Tenable: “Web Application Potentially Vulnerable to Clickjacking” (Plugin 85582)
- Burp Suite: “Frameable response (potential Clickjacking)”
- Qualys: “Missing X-Frame-Options HTTP Response Header” (QID 150078)
The scanner found the same thing every one of these warnings points to: your site does not tell browsers whether other people can embed your pages in iframes. That is what the warning means. Here is whether it matters and what to do about it.
What the scanner is checking
When a browser loads a web page inside an <iframe>, it looks for two response headers that control whether embedding is allowed:
X-Frame-Options— the original defense. Set toDENY(block all embedding) orSAMEORIGIN(allow only your own domain). Supported by every browser since Internet Explorer 8.Content-Security-Policy: frame-ancestors— the modern replacement. Supports multiple origins, wildcards, and the full CSP directive syntax. Supported in all browsers since January 2018 (Baseline Widely Available).
The scanner sends a request to your URL, reads the response headers, and checks whether either of these is present with a restrictive value. If neither header exists, it reports the finding.
Does this actually matter?
For most sites, yes. Here is the threat model:
An attacker creates a page that loads your site in a transparent iframe, layered under their own decoy buttons. A user who is already logged into your site visits the attacker's page, sees the decoy, clicks it, and their click lands on your real button instead — a “Delete Account” button, a money transfer confirmation, an OAuth authorization screen. The browser sends the user's real session cookies with the request because the request goes to your origin. The action executes.
This is not theoretical. Clickjacking has been used to:
- Trick users into authorizing OAuth apps
- Enable webcam and microphone permissions through Flash (historically)
- Force likes, follows, and reposts on social platforms
- Confirm one-click purchases on ecommerce sites
- Change router and IoT device settings on internal networks
The severity depends on what your page does. A static blog post with no authenticated actions is not a meaningful target — an attacker cannot do anything by tricking someone into clicking a paragraph of text. An authenticated dashboard, admin panel, or checkout flow is a different story.
If your site has any page where a user being tricked into clicking something could cause harm, the finding matters. If your site is purely static content with no forms, no authentication, and no state changes, the risk is minimal. Most sites fall in the first category.
The fix
Add both headers to every response your server sends. For most sites, the values are:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYCSP frame-ancestors covers all modern browsers. X-Frame-Options: DENY covers the small set of very old browsers that support XFO but not CSP Level 2. Setting both costs one extra header line and adds no meaningful response size.
If your own application frames its own pages — dashboards embedding widgets, live previews in an editor — use the same-origin variants instead:
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGINIf you need to allow specific third-party domains to embed your pages (a partner site embedding your widget, for example), use only CSP — X-Frame-Options cannot express multi-origin policies:
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.comDo not use X-Frame-Options: ALLOW-FROM. It was never supported by Chrome or Safari, and Firefox dropped it in 2019. Modern browsers ignore the entire header when they see it, leaving the page with no protection.
How to add the headers on your stack
The implementation depends on your server, CDN, or framework. The header must be set on the HTTP response — <meta> tags have no effect for either header.
| Platform | Snippet |
|---|---|
| NGINX | add_header X-Frame-Options "DENY" always; |
| Apache | Header always set X-Frame-Options "DENY" |
| Cloudflare | Transform Rules → Set X-Frame-Options: DENY |
| WordPress | header('X-Frame-Options: DENY'); |
Each platform has sharp edges beyond the one-liner — inheritance traps, duplicate header issues, proxy gotchas. The linked guides cover them in detail.
What about JavaScript frame-busting?
Some scanner documentation and older guides suggest adding a frame-busting script — JavaScript that detects when the page is inside an iframe and redirects the parent window:
if (top != self) { top.location = self.location; }This does not work. Attackers bypass it with the sandbox attribute, double framing, onbeforeunload handlers, no-content flushing, and CSP sandbox directives. The HTTP headers are enforced by the browser before the page renders — they cannot be bypassed by the page that embeds you.
Adding frame-busting JavaScript on top of headers adds no value: if the browser supports the headers, they block framing before the script ever runs. If the browser is old enough to lack header support, it is also old enough that multiple frame-busting bypasses work against it. Set the headers and skip the script.
Full breakdown: Why JavaScript Frame-Busting Doesn't Stop Clickjacking
Common mistakes that keep the finding open
- Setting the header on only some pages. Scanners check every URL they discover. If your login page has the header but your dashboard does not, the finding stays open. Add the header globally — at the web server or CDN level, not per-route.
- Using CSP-Report-Only instead of CSP.
Content-Security-Policy-Report-Onlylogs violations but does not enforce them. A Report-Onlyframe-ancestorsdirective provides zero clickjacking protection. Use the enforcedContent-Security-Policyheader. - Assuming
default-srccovers framing.frame-ancestorsis one of the few CSP directives that does not fall back todefault-src. A policy ofdefault-src 'self'allows any site to frame you. You must setframe-ancestorsexplicitly. - Trying to set the header in a meta tag. Neither
X-Frame-Optionsnorframe-ancestorsworks in<meta http-equiv>tags. They must be set as real HTTP response headers by your server or CDN. - Using
ALLOW-FROMin X-Frame-Options. This directive is obsolete. Modern browsers ignore the entire header when they see it, leaving the page with no protection at all. If you need to allow specific origins, use CSPframe-ancestors.
Verify the fix
After deploying the headers, confirm they are working before closing the scanner finding:
- Run your URL through the ClickJack Test tool. A “Protected” result means at least one of the headers is present and correctly configured.
- Check the response headers yourself with
curl -I https://yoursite.com. Look forx-frame-options: DENYorcontent-security-policy: ... frame-ancestors .... - Re-run your scanner. The finding should close once the headers appear on every response.
If the scanner re-opens the finding later, it likely crawled a page that is not receiving the headers — typically static assets served outside your application server, error pages, or API responses. Add the headers at the outermost layer (CDN, reverse proxy, or web server) so every response gets them.