All posts

Clickjacking protection when you cannot set a CSP header

CentralCSP Team ·

Last update:

Clickjacking protection needs a real HTTP response header. The Content Security Policy (CSP) directive that controls who can frame your page, frame-ancestors, is silently dropped when the policy comes from a <meta> tag, so a meta-delivered CSP gives you no framing protection at all. If you cannot set headers freely, the host-limited fallback is X-Frame-Options, which is simpler and more constrained. This post explains the meta-tag gap, what X-Frame-Options can and cannot do, and what to reach for when your only options are limited headers or a meta tag.

frame-ancestors is dropped in a meta tag

frame-ancestors decides which origins may embed your page in a frame, which is your defense against clickjacking (an attacker framing your page invisibly and tricking a user into clicking through it). It works when the policy arrives as the Content-Security-Policy response header. It does not work from a meta tag.

The CSP specification lists frame-ancestors among the directives that are ignored when the policy is delivered in a <meta http-equiv> element. The browser keeps the rest of the meta policy and quietly discards this directive. There is no console error telling you the page is unprotected, which is what makes it dangerous: the policy looks present, but the framing rule is gone.

<!-- frame-ancestors here is silently ignored, no clickjacking protection -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'">

This is one of several directives that only work as a header. The full list and the reasoning are in CSP meta tag vs HTTP header. The takeaway for clickjacking is narrow and absolute: if you can only emit a meta tag, you cannot use frame-ancestors, and you need a different mechanism.

X-Frame-Options, the host-limited fallback

When you cannot set a CSP header but can set some response headers, X-Frame-Options is the older header that controls framing. Some hosts and CDNs expose a short list of allowed security headers even when they do not let you set an arbitrary CSP, and X-Frame-Options is often on that list. It has two usable values:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

DENY stops every site, including your own, from framing the page. SAMEORIGIN allows only your own origin to frame it. That is the whole vocabulary. The header is coarse on purpose, which is both its limit and the reason it still works as a fallback.

The constraints matter:

  • No allowlist of multiple origins. frame-ancestors can name several trusted embedders (frame-ancestors 'self' https://partner.example). X-Frame-Options cannot. You get same-origin or nobody, and no way to permit one named partner.
  • ALLOW-FROM is deprecated and does not work. The old X-Frame-Options: ALLOW-FROM https://partner.example value was meant to allow a single external embedder. Modern browsers ignore it. Do not rely on it; if you need to allow a specific partner to frame you, that requires frame-ancestors, which requires a real header.
  • frame-ancestors supersedes it. When a page sends both, browsers honour the CSP directive and ignore X-Frame-Options (see X-Frame-Options vs frame-ancestors for the full comparison). So X-Frame-Options is the fallback you use only when frame-ancestors is not available to you.

For the directive's own behaviour, its syntax, and how it relates to the legacy header in detail, see the frame-ancestors directive reference. This post is the practical "what do I do when I can't set the header" version; the reference is the complete one.

What to do with limited headers or only a meta tag

Match the fix to what your host actually lets you control.

You can set arbitrary response headers. Use frame-ancestors on a real Content-Security-Policy header and skip X-Frame-Options entirely. frame-ancestors 'none' to block all framing, frame-ancestors 'self' for same-origin only, or a named allowlist for specific partners. This is the modern, expressive option and the one to prefer whenever it is available.

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

You can set only a short list of security headers (no CSP). Use X-Frame-Options: DENY if nothing should frame the page, or SAMEORIGIN if your own pages frame it. Accept that you cannot allow a named external partner from here. If a partner genuinely needs to embed you, that is the case where you have to find a way to set a CSP header, there is no header-limited shortcut for it.

You can only emit a meta tag. You have no clickjacking protection from CSP, because frame-ancestors is ignored in a meta tag and X-Frame-Options cannot be set from markup at all (it is a response header with no meta-tag equivalent). The honest answer is that markup alone cannot defend against framing. Treat this as a reason to get even one real security header configured, through the platform, a reverse proxy, an edge worker, or a different host, rather than something a <meta> tag can paper over.

X-Frame-Options is a legacy header, and once you have a CSP with frame-ancestors it is redundant. It is one of the headers worth removing at that point, covered in legacy security headers to retire. Keep it only as the fallback while you cannot set CSP.

Verify what you actually shipped

Because the meta-tag failure is silent, check the response your server really sends rather than trusting the markup. The free security headers scanner reads the deployed response headers and shows whether frame-ancestors (in CSP) or X-Frame-Options is present and what value it carries, so you can confirm the page is not framable when you intended it not to be.

If clickjacking protection is part of a wider need to watch what your pages send and load over time, the CentralCSP CSP suite monitors your CSP and the scripts on each page from real traffic, not just a one-off scan. You can start a free trial to see the reports and the header state across your site.

Sources