Skip to content
ClickJack Test

← Blog

Missing Anti-Clickjacking Header? What It Means and How to Fix It

Published July 2026 · Updated October 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: “Missing Anti-clickjacking Header” (alert 10020-1, Medium)
  • Nessus / Tenable: “Web Application Potentially Vulnerable to Clickjacking” (plugin 85582, Medium)
  • Burp Suite: “Frameable response (potential Clickjacking)” (Information)
  • Qualys: a missing X-Frame-Options finding (WAS QID 150245, informational)

All of these mean the same thing: the response does not tell browsers whether other sites may embed the page in an iframe. Scanners map it to CWE-1021 (Improper Restriction of Rendered UI Layers or Frames). 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 to DENY (block all embedding) or SAMEORIGIN (allow only your own origin). Browsers have supported it since Internet Explorer 8.
  • Content-Security-Policy: frame-ancestors: the modern replacement. It supports multiple origins, wildcards, and the full CSP source syntax, and it has worked in all major browsers since January 2018. When a browser enforces it, the browser ignores X-Frame-Options entirely.

Scanners read the response headers and report the finding when neither header is present. They check presence, not strength, and the details differ by tool:

  • ZAP (passive rule 10020) accepts any Content-Security-Policy header that contains frame-ancestors without parsing the value, so frame-ancestors * clears the finding. Without it, ZAP wants an X-Frame-Options value containing deny or sameorigin. At default settings it only examines HTML responses that are not redirects or errors.
  • Nessus plugin 85582 and Burp's “Frameable response” check accept either header. Burp reports it as Information because it only tells you to check whether the page has sensitive actions.

Does this actually matter?

For any site with logins or state-changing actions, 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 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 request goes to your origin, so if the browser attaches the user's session cookie, the action executes as them.

Cookies marked SameSite=Lax or Strict are not sent on requests made from a cross-site iframe, which blunts this. It does not help with unauthenticated actions or attacks from sibling subdomains, so OWASP recommends it alongside the framing headers, not instead of them.

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

Severity depends on what the page does. An authenticated dashboard, admin panel, login page, or checkout flow is a real target. The finding is low risk, or a false positive, when:

  • The URL is an API endpoint returning JSON. There is no interface to overlay and nothing to click. ZAP skips non-HTML responses by default; other scanners may not.
  • It is a static asset such as an image, stylesheet, script, or font.
  • The page has no authentication, no forms, and no state changes, such as a public blog post. Tricking someone into clicking a paragraph of text achieves nothing. Tenable notes this exact false-positive case for plugin 85582.
  • The page is meant to be embedded, such as a widget or video player. The fix there is an allow-list of embedding origins, not a block.

Even then, setting the headers globally is a one-line config change and closes the finding. If you accept the risk instead, document the exception per URL rather than per site.

The fix

Send both headers on every response, or at least every HTML page. For most sites, the values are:

Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY

CSP frame-ancestors covers all modern browsers, and when it is enforced, those browsers ignore X-Frame-Options. X-Frame-Options: DENY only matters for very old browsers that understand XFO but not CSP Level 2. Setting both costs one extra header line.

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: SAMEORIGIN

If 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, and a leftover SAMEORIGIN would be applied by old browsers and block your partner:

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com

Do not use X-Frame-Options: ALLOW-FROM. Chrome and Safari never supported it, and Firefox dropped it in version 70 (2019). Browsers that do not recognize the value ignore the whole header, leaving the page unprotected. Use CSP frame-ancestors instead.

How to add the headers on your stack

The implementation depends on your server, CDN, or framework. The headers must be set on the HTTP response. <meta> tags have no effect for either one.

PlatformSnippet
NGINXadd_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
ApacheHeader always set Content-Security-Policy "frame-ancestors 'none'"
Header always set X-Frame-Options "DENY"
CloudflareTransform Rules → Modify Response Header → Set static: X-Frame-Options = DENY
Content-Security-Policy = frame-ancestors 'none'
WordPressadd_action('send_headers', function () { header("Content-Security-Policy: frame-ancestors 'none'"); header('X-Frame-Options: DENY'); });

Each platform has sharp edges beyond these snippets: inheritance traps, duplicate headers, proxy gotchas. The linked guides cover them in detail.

What about JavaScript frame-busting?

Some older guides suggest 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; }

It is easy to bypass. Attackers use the sandbox attribute on the iframe, double framing, onbeforeunload handlers, and no-content flushing. The HTTP headers are enforced by the browser before the page renders, and the page that embeds you cannot bypass them.

On top of the headers, a script adds little: browsers that support the headers block framing before the script runs. OWASP treats a defensive script only as a supplement for legacy browsers, never as the primary defense. Set the headers and skip the script.

Full breakdown: Why JavaScript Frame-Busting Doesn't Stop Clickjacking

Common mistakes that keep the finding open

  1. 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.
  2. Using CSP-Report-Only instead of CSP. Content-Security-Policy-Report-Only logs violations but does not enforce them. A Report-Only frame-ancestors directive provides zero clickjacking protection. Use the enforced Content-Security-Policy header.
  3. Assuming default-src covers framing. frame-ancestors is one of the few CSP directives that does not fall back to default-src. A policy of default-src 'self' allows any site to frame you. You must set frame-ancestors explicitly.
  4. Trying to set the header in a meta tag. Neither X-Frame-Options nor frame-ancestors works in <meta http-equiv> tags, and ZAP reports an X-Frame-Options meta tag as alert 10020-3. Both must be real HTTP response headers from your server or CDN.
  5. Using ALLOW-FROM in X-Frame-Options. This directive is obsolete. Modern browsers ignore the entire header when they see it, leaving the page unprotected, and ZAP flags it as malformed (alert 10020-4). To allow specific origins, use CSP frame-ancestors.
  6. Allowing everyone in frame-ancestors. When an enforced CSP contains frame-ancestors, browsers ignore X-Frame-Options entirely. So frame-ancestors * plus X-Frame-Options: DENY leaves the page open to any site. ZAP only checks that the directive exists, so the scanner can close the finding while the page stays frameable. Check the value, not just the presence.
  7. Sending duplicate or conflicting X-Frame-Options headers. This usually happens when the app and a proxy both add the header. ZAP reports multiple entries as alert 10020-2, and browsers treat conflicting values (such as DENY and SAMEORIGIN) as a block, so framing fails even for your own pages. Set each header in one layer only.

Verify the fix

After deploying the headers, confirm they are working before closing the scanner finding:

  1. Run your URL through the ClickJack Test tool. A “Protected” result means the headers you send would actually stop browsers from framing the page, taking the rule that frame-ancestors overrides X-Frame-Options into account.
  2. Check the response headers yourself with curl -s -o /dev/null -D - https://yoursite.com. Look for content-security-policy: ... frame-ancestors ... and x-frame-options: DENY. This sends a GET, so you see what browsers and scanners see.
  3. Re-run your scanner. The finding should close once the headers appear on every response it checks.

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. These headers also do not stop DoubleClickjacking, which works through separate windows rather than frames.

Test your site now →

Check your headers

See whether a site sends X-Frame-Options or CSP frame-ancestors, and what browsers will do with them.