Clickjacking prevention starts with two HTTP headers that most sites still get wrong. This guide explains X-Frame-Options, CSP frame-ancestors, and how UI redressing attacks work.
Anay Pandya
1,510 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
Clickjacking prevention is a critical yet often overlooked aspect of web application security. A clickjacking attack — also called a UI redressing attack — tricks users into clicking on something different from what they perceive, by overlaying a transparent iframe over a legitimate-looking UI. One click can unknowingly authorize a payment, change account settings, or enable a webcam.
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">In our analysis of 12,400+ sites scanned on ZeriFlow, 64% lack a Content-Security-Policy header — and of those that have one, 71% use 'unsafe-inline', negating XSS protection entirely.</p> </div>
Keep reading
Is your site actually secure?
Run a free check — 60 seconds
Find out if your site is vulnerable to clickjacking in seconds — [scan it free with ZeriFlow](https://zeriflow.com).
The mechanics are deceptively simple. An attacker creates a malicious page that loads your web application inside an invisible or transparent <iframe>. They then overlay decoy content — a fake button, a game element, a social share prompt — precisely over a sensitive action on your framed page.
<!-- Attacker's malicious page -->
<style>
iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
opacity: 0.0001; /* invisible but clickable */
z-index: 2;
}
.decoy-button {
position: absolute;
top: 200px; left: 300px;
z-index: 1;
}
</style>
<div class='decoy-button'>Click here to claim your prize!</div>
<iframe src='https://victim-bank.com/transfer?to=attacker&amount=1000'></iframe>When the victim clicks the "prize" button, they're actually clicking the bank's transfer confirmation button on the transparent iframe.
X-Frame-Options is an HTTP response header that controls whether a browser is allowed to render a page inside a <frame>, <iframe>, <embed>, or <object>.
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://trusted.com| Directive | Meaning |
|---|---|
DENY | Page cannot be framed by anyone |
SAMEORIGIN | Page can only be framed by pages on the same origin |
ALLOW-FROM uri | Page can be framed only by the specified URI |
Recommendation: Use DENY for all pages that don't need to be embedded. Use SAMEORIGIN if your own app embeds its pages. Avoid ALLOW-FROM — it's deprecated and not supported in Chrome or Firefox.
Nginx:
add_header X-Frame-Options 'DENY' always;Apache:
Header always set X-Frame-Options "DENY"Express.js (Node.js):
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));Django:
# settings.py
X_FRAME_OPTIONS = 'DENY'
MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# ...
]While X-Frame-Options is widely supported and still effective, Content Security Policy (CSP) `frame-ancestors` is the modern, more flexible replacement. It's part of the CSP Level 2 specification and takes precedence over X-Frame-Options in browsers that support it.
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;ALLOW-FROM in X-Frame-Options only supports one URI. CSP frame-ancestors supports multiple.frame-ancestors 'self' https://*.trusted.com allows all subdomains.Content-Security-Policy: frame-ancestors 'self' https://app.partner.com; report-uri /csp-violationsFor maximum compatibility (older browsers, IE), deploy both headers:
add_header X-Frame-Options 'DENY' always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;CSP frame-ancestors takes precedence in modern browsers; X-Frame-Options covers the rest.
ZeriFlow's security scanner automatically checks for:
X-Frame-Options headerframe-ancestors in your CSP headerRun a free clickjacking scan on your site at ZeriFlow — results in under 60 seconds, no signup required.
Before X-Frame-Options existed, developers used JavaScript "frame-busting" scripts to detect when their page was framed and redirect to the top-level window:
// Old-school frame-busting (DO NOT rely on this)
if (top !== self) {
top.location = self.location;
}This is easily defeated. Attackers can sandbox the iframe to disable JavaScript:
<iframe src='victim.com' sandbox='allow-forms allow-scripts'></iframe>Or use onBeforeUnload tricks. Never rely on JavaScript frame-busting as your primary defense. Use HTTP headers.
Some attacks use HTML5 drag-and-drop events rather than clicks. The user drags an element on the attacker's page but is actually interacting with a framed target. This can be used to extract text content from framed cross-origin pages.
Mitigation: Same X-Frame-Options/frame-ancestors headers prevent the frame from loading at all.
Instead of a single click, attackers guide users through multiple interactions — each one triggering a step in a multi-page workflow (like a multi-step bank transfer confirmation). The attacker needs to precisely align each step.
Mitigation: CSRF tokens on each step make it much harder to craft a working multi-step attack even if framing succeeds.
Attackers can manipulate the apparent cursor position using CSS, making users think their cursor is in one location while it's actually elsewhere. Combined with clickjacking, this is particularly deceptive.
1. Only protecting login pages Sensitive actions exist throughout your app — password changes, payment confirmations, permission grants. Apply framing protection to all pages, not just login.
2. Using X-Frame-Options on CDN assets Static assets (images, scripts) don't need framing protection. Apply headers selectively to HTML responses.
3. Setting SAMEORIGIN when DENY is appropriate
If your app doesn't use iframes internally, use DENY. SAMEORIGIN only makes sense if you deliberately embed your own pages.
4. Forgetting the `always` flag in Nginx/Apache
Without always, the header isn't sent on error responses (4xx, 5xx). Attackers can sometimes frame error pages to extract information.
5. Not testing after deployment Header misconfiguration is common. Tools like ZeriFlow can catch this automatically.
A: No. HTTPS secures the transport layer but has no bearing on whether your page can be framed. Clickjacking attacks work perfectly over HTTPS. You need X-Frame-Options or CSP frame-ancestors headers regardless of your SSL checker configuration.
A: If you need your site to be embeddable (a widget, a map, a video player), use CSP frame-ancestors with specific allowed origins rather than DENY. This gives you fine-grained control — blocking unauthorized framing while allowing trusted partners.
A: Native mobile apps aren't affected since they don't use browser iframes. However, mobile web apps (running in a browser or WebView) are vulnerable to the same clickjacking attacks as desktop web apps. Apply the same header-based protections.
A: Clickjacking falls under OWASP A05:2021 (Security Misconfiguration) in the current Top 10. It's also historically noted as a significant vulnerability in the OWASP Testing Guide. While not always ranked #1, it's consistently considered a high-priority misconfiguration to fix.
A: The simplest test is to create an HTML file with an iframe pointing to your site and open it in a browser. If the page loads in the iframe, you're vulnerable. Automated tools like ZeriFlow check this instantly, along with 80+ other security issues.
Clickjacking prevention is one of the highest-ROI security fixes you can make — two HTTP headers eliminate the attack surface entirely. X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' should be in your default server configuration for every web application.
The gap between knowing about clickjacking and actually being protected is often just a missing header. [Scan your site with ZeriFlow right now](https://zeriflow.com) to see if you're protected — it takes less than a minute and covers 80+ security checks beyond just clickjacking.
Don't let a two-line fix be the reason a user's account gets compromised.
<!-- zf-internal-links -->
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
June 24, 2026
Best Security Tools for AI Startups
Monitor your security score
Schedule recurring scans and catch regressions after deployments.