What Is Clickjacking? And the One Header That Stops It
Clickjacking tricks a user into clicking something on your site while they think they're clicking something else, by loading your page invisibly inside an attacker's page. How it works, what it can do, and the fix: CSP frame-ancestors and X-Frame-Options — with examples for Nginx, Next.js and Express.
Clickjacking is an attack where someone tricks your users into clicking a button on your site without realising it.
How it works
- The attacker makes a web page — a game, a "claim your prize" page, anything enticing.
- On it, they load your site inside an
<iframe>and make it invisible (opacity: 0), positioned exactly over their own button. - The victim, already logged in to your site, clicks what looks like "Play" on the attacker's page.
- The click actually lands on your invisible page — on "Delete account", "Make public", "Confirm transfer" or "Allow access".
Because the user is logged in, your site sees a genuine click from a genuine session.
What it can do
Anything a single click (or a few) can do on your site:
- change settings (make a profile public, disable 2FA)
- approve OAuth or permission requests
- like, follow or share
- confirm a purchase or transfer where there's no extra confirmation step
The fix: don't let other sites frame you
Tell browsers whether your pages may be shown inside frames on other sites. There are two headers.
Content-Security-Policy: frame-ancestors (modern)
Content-Security-Policy: frame-ancestors 'self'
'none'— no one may frame your pages.'self'— only your own site may.'self' https://partner.example.com— your site and a specific partner.
(Content Security Policy guide)
X-Frame-Options (older, still useful)
X-Frame-Options: DENY
or SAMEORIGIN. Older browsers understand it; modern browsers prefer frame-ancestors. Sending both is common and harmless.
Setting it
Nginx:
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Express, with Helmet (which sets these by default):
import helmet from 'helmet'
app.use(helmet())
Next.js (next.config.js):
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
],
}]
}
Caddy:
header {
Content-Security-Policy "frame-ancestors 'self'"
X-Frame-Options "SAMEORIGIN"
}
If you already have a CSP header, add frame-ancestors to it rather than sending a second one.
Check it
Open DevTools → Network → your page → Response Headers. Or use an online security-headers checker. (HTTP security headers explained)
A quick manual test: make a local HTML file with <iframe src="https://yoursite.com"></iframe>. If the headers work, the browser refuses to show it.
When you do need framing
If your product is meant to be embedded — a widget, a booking form, a chat box on customers' sites:
- Allow framing only on the specific pages meant to be embedded, not your whole app (especially not account settings).
- List the allowed parent domains in
frame-ancestorsif you know them. - Require an extra confirmation step for anything sensitive inside the embed.
Also helps
- SameSite cookies —
SameSite=Lax(the browser default) orStrictmeans your session cookie isn't sent when your site is framed by another site, so the framed page isn't logged in. (CSRF and SameSite) - Confirmation for destructive actions — retyping a name, re-entering a password.
EasySpawn serves your app behind a reverse proxy with sensible security headers already in place, including protection against being framed. See how it works or join the waitlist.
Related: HTTP Security Headers Explained · Content Security Policy · CSRF Explained · The OWASP Top 10 Explained
Keep reading
"Your Connection Is Not Private" on Your Own Site: Causes and Fixes
When visitors see NET::ERR_CERT_DATE_INVALID, ERR_CERT_COMMON_NAME_INVALID or ERR_CERT_AUTHORITY_INVALID on your site, the SSL certificate is expired, for the wrong name, or incomplete. How to tell which, and how to fix each one.
What Is HSTS? Strict-Transport-Security Explained
HSTS tells browsers to only ever use HTTPS for your site, closing the gap where a first HTTP request could be intercepted. How the header works, max-age, includeSubDomains and preload, how to roll it out safely, and how to set it in Nginx, Caddy, Express and Next.js.