X-Frame-Options: Stop Clickjacking With One Header
X-Frame-Options is a response header that tells browsers whether other sites may show your page in a frame. Send DENY or SAMEORIGIN, plus CSP frame-ancestors.

On this page
- What is X-Frame-Options?
- What is clickjacking, and why does it matter for your app?
- What's the difference between DENY and SAMEORIGIN?
- Should you use CSP frame-ancestors instead?
- How do you set X-Frame-Options on Vercel, Netlify and Next.js?
- What does a clickjacking attack look like, step by step?
- Which pages need the header?
- What mistakes should you avoid?
- How do you check if X-Frame-Options is working?
- What else belongs with it?
- FAQ
Short answer: X-Frame-Options is an HTTP response header that tells browsers whether other websites may show your page inside a frame. Set X-Frame-Options: DENY (or SAMEORIGIN if your own site frames itself) to block clickjacking, where an attacker hides your real app under a fake button. The modern equivalent is CSP frame-ancestors; sending both is the safest choice.
By Gary, Greenlight · Last checked against MDN and OWASP: Oct 20, 2026 · Drafted with AI tools and reviewed before publishing.
What is X-Frame-Options?
It's one line your server sends with each page. Browsers read it before showing your page inside an iframe, frame, embed or object element on another site, and refuse if the header says no (MDN (external)). It was written up as RFC 7034 in October 2013 (RFC 7034 (external)).
What is clickjacking, and why does it matter for your app?
Clickjacking tricks a signed-in user into clicking something on your app without knowing it. The attacker's page loads your app in an invisible frame and lines up a button ("Delete account", "Send", "Make public") under something harmless like "Play video". OWASP lists the X-Frame-Options and CSP frame-ancestors headers as defenses (OWASP cheat sheet (external)).
If your app has sign-in and any button that changes data, you want this header.
What's the difference between DENY and SAMEORIGIN?
| Value | Who may frame your page | Use when |
|---|---|---|
DENY | nobody, not even you | most apps (best default) |
SAMEORIGIN | pages on your own origin only | your app frames its own pages (e.g. a preview pane) |
ALLOW-FROM uri | obsolete | don't use it; MDN says modern browsers ignore it (MDN (external)) |
If you need to allow one specific partner site, use CSP frame-ancestors instead (next section).
Should you use CSP frame-ancestors instead?
Use both. frame-ancestors is part of Content-Security-Policy and does the same job with more control: you can list exact sites (MDN (external)). OWASP quotes the CSP spec: when a page sends an enforced frame-ancestors policy, the X-Frame-Options header "MUST be ignored." OWASP adds that some older browsers followed X-Frame-Options instead, and recommends using more than one defense where possible (OWASP (external)). Sending both covers old and new browsers.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'Equivalents: DENY ↔ frame-ancestors 'none', SAMEORIGIN ↔ frame-ancestors 'self'. To allow one partner: frame-ancestors 'self' https://partner.example.
One catch: MDN says frame-ancestors "is not supported in the <meta> element" (MDN (external)), and that setting X-Frame-Options in a <meta> tag "has no effect" (MDN (external)). Both have to be real response headers.
How do you set X-Frame-Options on Vercel, Netlify and Next.js?
Vercel (vercel.json) (Vercel docs (external)):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" }
]
}
]
}Netlify (public/_headers or netlify.toml) (Netlify docs (external)):
/*
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'Next.js (next.config.js) (Next.js docs (external)):
module.exports = {
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
],
}];
},
};If you already send a Content-Security-Policy, add frame-ancestors 'none' to it instead of sending a second CSP header.
Building with Lovable or Bolt? Where your app is hosted decides where headers go. If you deploy to your own Vercel or Netlify account, the files above apply. If you use the builder's own hosting, check its docs for custom header support; it may simply be a setting that isn't on yet. Our Lovable review covers what Lovable's hosting includes.
What does a clickjacking attack look like, step by step?
Here's the pattern OWASP describes, made concrete for a typical AI-built app with a "Make my project public" button (OWASP (external)):
- The attacker builds a page with something people want to click, such as a "Claim your prize" button.
- On top of it they load your app in an
iframe, sized and positioned so your real "Make public" button sits exactly over theirs, and set the frame's opacity to 0 so it's invisible. - They send the link to your users. Anyone who is already signed in to your app sees only the prize page.
- The click lands on your invisible app. Because the browser sends the user's own session with the request, your app sees a normal click from a signed-in user and does what it was told.
Nothing about your sign-in or database rules was broken. The browser simply let another site show your page. That's why the fix lives in a response header: it tells the browser not to show your page inside someone else's frame at all.
Which pages need the header?
All of them, ideally. Setting it on /(.*) (every path) is simpler and safer than picking pages. The pages that matter most are anything behind sign-in, settings and account pages, payment or checkout steps, and any page with a one-click action such as delete, share, invite or publish. Your public marketing pages are lower risk, but blocking framing there costs nothing unless you embed them elsewhere.
If one route must be embeddable (for example a public widget), give that route its own frame-ancestors list of allowed sites and keep 'none' everywhere else. Order matters in some hosts' header files, so check the host's docs for how overlapping rules combine.
What mistakes should you avoid?
- Setting it only in HTML. A
<meta http-equiv>tag doesn't work for either header (MDN (external); OWASP (external)). - Sending two different values. OWASP notes that browsers "only honor one X-Frame-Options header and only one value on that header," and that proxies often add or strip headers (OWASP (external)). If a proxy adds
SAMEORIGINand your app addsDENY, the result is unpredictable. Send it from one place. - Using
ALLOW-FROM. It's obsolete and ignored by modern browsers (MDN (external)). - Forgetting preview and staging URLs. If they share real accounts or data, they need the header too.
- Breaking your own embeds. If you show your app inside your own dashboard on a different domain,
DENYwill blank it. List that domain inframe-ancestorsinstead.
How do you check if X-Frame-Options is working?
From a terminal:
curl -sI https://yourapp.com | grep -iE "x-frame-options|content-security-policy"You should see your values. Then try it in a browser: open a blank tab (about:blank), open the developer console, and paste this. The frame should stay blank, with a refusal message in the console.
const f = document.createElement("iframe");
f.src = "https://yourapp.com";
f.width = 600; f.height = 400;
document.body.append(f);Our security headers checker also shows which headers your site sends.
What else belongs with it?
X-Frame-Options is one of a small set of headers most apps should send: HSTS (always use HTTPS), Content-Security-Policy, X-Content-Type-Options and Referrer-Policy. Our Lovable launch checklist covers the rest of a pre-launch pass, what is vibe coding? explains why AI-built apps often ship without these headers, the Next.js security update guide covers keeping the framework patched, and what is a .env file? covers keeping secrets out of the browser.
FAQ
Is X-Frame-Options deprecated? OWASP says it "has been obsoleted in favor of the frame-ancestors directive" from CSP Level 2, though major browsers still support it (OWASP (external)). Send both.
Will DENY break my app?
Only if something frames it on purpose (an embed widget, your own preview pane). Use SAMEORIGIN or a frame-ancestors allow-list then.
Can I set it in HTML? No. Both must be HTTP response headers.
Sources
- MDN, X-Frame-Options (external)
- MDN, CSP frame-ancestors (external)
- OWASP, Clickjacking Defense Cheat Sheet (external)
- RFC 7034, HTTP Header Field X-Frame-Options (external)
- Vercel docs, Project configuration (headers) (external)
- Next.js docs, headers in next.config.js (external)
- Netlify docs, Custom headers (external)
On this site
Get the next change before it bites
Greenlight tips and new blog posts: short, practical notes when Supabase, Lovable, Bolt, Cursor or another tool you build with changes something that affects your app, plus what to check. About twice a month.
We'll send a confirmation email first. Unsubscribe anytime. Privacy. Greenlight My App is independent and not affiliated with or endorsed by Supabase, Lovable, Bolt, Cursor or any other tool we mention.
Greenlight is independent and not affiliated with or endorsed by Vercel, Netlify, Next.js, Lovable, Bolt, Mozilla (MDN), OWASP or the IETF. Product names are used only to identify the tools this guide is about. This page is general information, not professional security advice, legal advice, a security audit or a guarantee. Check your own setup against the vendor's current docs.