The short version
Clickjacking is an attack where someone loads your website inside an invisible iframe on their own page. They position the iframe so that when a user clicks on something that looks innocent (like a "Play Video" button), they're actually clicking a button on your hidden site.
It's classified as CWE-1021: Improper Restriction of Rendered UI Layers.
How it works
Here's the setup:
- The attacker creates a page with something enticing. Maybe a game, a video, a prize.
- They embed your site in an iframe on that page, making the iframe invisible (opacity: 0) or positioned off-screen.
- They align your site's buttons with their fake buttons.
- When the user clicks, they interact with your site without knowing it.
For example: the attacker could position your "Delete Account" button right under their "Click Here to Win" button. The user clicks, and their account is gone.
The OWASP Clickjacking guide shows this in detail with examples.
The fix: X-Frame-Options
The simplest defense is the X-Frame-Options HTTP header. As documented on MDN, it has two values:
X-Frame-Options: DENY
No one can embed your site in an iframe. Period.
X-Frame-Options: SAMEORIGIN
Only pages on the same origin can embed your site.
That's it. One header. If your site has no reason to be embedded in an iframe, use DENY.
The modern alternative: CSP frame-ancestors
The frame-ancestors directive in Content Security Policy is the modern replacement:
Content-Security-Policy: frame-ancestors 'none'
This is equivalent to X-Frame-Options: DENY. You can also specify allowed origins:
Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com
The advantage of frame-ancestors over X-Frame-Options is flexibility. You can whitelist specific domains, not just "same origin" or "nobody."
Use both headers for backward compatibility. Older browsers might not support CSP but will respect X-Frame-Options.
Why this matters for vibe coders
If you built your app with a tool like Bolt.new, Lovable, or Cursor, there's a good chance neither of these headers is set. Most code generators focus on features, not security headers.
Without these headers, anyone can embed your app in an iframe. If your app has any authenticated actions (delete account, change password, make a purchase), those are all vulnerable to clickjacking.
How to add the header
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
Netlify (_headers file):
/*
X-Frame-Options: DENY
Next.js (next.config.js):
headers: [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' }
]
}
]
Edge cases
Embedding third-party payment forms. If you use Stripe Elements or similar embedded widgets, those use iframes. Your CSP/X-Frame-Options won't affect them because they control their own headers, not yours.
Legitimate embeds. If you build a widget that others embed (like a chat widget), you can't use DENY. Use frame-ancestors with a list of allowed domains, or authenticate the embedding context.
Check your site
Want to know if your site has this issue? Scan it now and find out in 60 seconds.
Frequently Asked Questions
- What is clickjacking?
- Clickjacking is an attack where a malicious site embeds your site in an invisible iframe, then tricks users into clicking buttons they can't see. The user thinks they're clicking on the attacker's page, but they're actually interacting with your site.
- What's the difference between X-Frame-Options and CSP frame-ancestors?
- They do the same thing: control whether your site can be embedded in iframes. CSP frame-ancestors is the modern replacement and more flexible. X-Frame-Options is older but still widely supported.
- Should I block all iframes?
- Unless you have a specific reason to allow embedding (like a widget or embed feature), yes. Set X-Frame-Options: DENY or CSP frame-ancestors 'none'.
Your AI writes the code. We find what it missed.
Paste your URL. Security audit in 60 seconds.
Scan my app