Missing X-Frame-Options: add clickjacking protection with frame-ancestors
A "missing X-Frame-Options" warning means your pages can be loaded inside another site's frame, which is how clickjacking works. Send Content-Security-Policy: frame-ancestors 'none' (or 'self' if your own site frames its pages), and X-Frame-Options: DENY (or SAMEORIGIN) for older browsers. Both must be HTTP response headers: a <meta> tag doesn't work for either.
What the headers do
MDN: X-Frame-Options indicates "whether a browser should be allowed to render the document in a <frame>, <iframe>, <embed> or <object>," so sites can "avoid clickjacking attacks and some cross-site leaks." Without either header or frame-ancestors, MDN notes, "the browser will allow other sites to embed this document."
In a clickjacking attack, another page loads yours in a transparent frame and tricks the visitor into clicking your buttons (delete account, confirm payment) while they think they're clicking something else.
| Header | Blocks every site | Allows your own site only |
|---|---|---|
Content-Security-Policy |
frame-ancestors 'none' |
frame-ancestors 'self' |
X-Frame-Options |
DENY |
SAMEORIGIN |
MDN says frame-ancestors 'none' "is similar to X-Frame-Options: deny (which is also supported in older browsers)", and that the old ALLOW-FROM value is obsolete: browsers "will ignore the header completely" if they see it. To allow a specific partner site, use frame-ancestors 'self' https://partner.example.
Add the headers
Netlify or Cloudflare Pages (_headers file):
/*
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
If you already send a Content-Security-Policy header, add frame-ancestors 'none' to it as one more directive (separated by ;) instead of sending a second CSP header. Test this on a preview deployment before relying on it.
Before you switch it on
- Do you embed your own pages? An app that shows its own pages in an iframe needs
'self'/SAMEORIGIN, not'none'/DENY. - Do partners embed you? A widget or booking page meant to be embedded elsewhere needs those sites listed in
frame-ancestors, and noX-Frame-Options, sinceDENYandSAMEORIGINcan't allow other sites. - Builder previews: some app builders show your app inside their editor in a frame. Their preview may stop loading your live domain once you block framing; your own domain still works normally.
What Pewterkit Check looks at
The check reads your home page's response headers and counts clickjacking protection as present when there is an X-Frame-Options header or a Content-Security-Policy header that contains frame-ancestors. Missing protection is reported together with the other basic browser protections (HSTS, nosniff, a referrer policy) in one finding. It skips builder subdomains, where the platform controls the headers.
FAQ
Do I need both headers?
frame-ancestors is the modern control, and X-Frame-Options covers older browsers. Sending both with matching rules is common and harmless.
Why doesn't my meta tag work?
MDN says setting X-Frame-Options in a <meta> element "has no effect", and that frame-ancestors "is not supported in the <meta> element." Use response headers.
Which should I choose, DENY or SAMEORIGIN?
DENY if your app never frames its own pages, SAMEORIGIN if it does. Start with DENY and switch if something on your own site stops showing.
Sources
- X-Frame-Options header (MDN) — read 27 Oct 2026
- CSP: frame-ancestors (MDN) — read 27 Oct 2026
- Custom headers (Netlify Docs) — read 27 Oct 2026
- Headers (Cloudflare Pages docs) — read 27 Oct 2026
- Static configuration with vercel.json: headers (Vercel Docs) — read 27 Oct 2026
How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked Oct 27, 2026. Spotted something out of date? Tell us.