Introduction
Clickjacking occurs when an attacker embeds your application in an invisible iframe on a malicious website. The victim interacts with what they believe is the attacker's page, but their clicks are actually sent to your application. Without X-Frame-Options or Content-Security-Policy: frame-ancestors headers, any website can embed your application in an iframe, enabling attacks like invisible button overlays, form hijacking, and unauthorized state-changing actions.
Symptoms
- Security scanner reports missing
X-Frame-Optionsheader: `- [MEDIUM] Missing X-Frame-Options header
- https://your-app.example.com/dashboard
- Response does not include X-Frame-Options or Content-Security-Policy frame-ancestors
`- Reports from users or bug bounty program of the site being embedded in malicious iframes
- Unexpected state-changing actions from users who did not intend them:
`- User changed password but reports not initiating the action
- User transferred funds but reports clicking a different button
`- curl shows no frame protection headers:
- ```bash
- curl -sI https://your-app.example.com | grep -iE "x-frame|content-security"
- # (no output - headers missing)
`
Common Causes
- Web server not configured to send
X-Frame-Optionsheader - Content-Security-Policy header missing
frame-ancestorsdirective - Application framework not configured with clickjacking protection middleware
- Header deliberately removed to allow legitimate iframe embedding
- Proxy or CDN stripping security headers
Step-by-Step Fix
Phase 1: Verify the Vulnerability
- 1.Confirm missing headers:
- 2.```bash
- 3.curl -sI https://your-app.example.com/
- 4.# Check for:
- 5.# X-Frame-Options: DENY or SAMEORIGIN
- 6.# Content-Security-Policy: frame-ancestors 'none' or 'self'
- 7.
` - 8.Test iframe embedding:
- 9.```html
- 10.<!-- Save as test-clickjacking.html and open in browser -->
- 11.<html>
- 12.<body>
- 13.<h1>Clickjacking Test</h1>
- 14.<iframe src="https://your-app.example.com/login"
- 15.width="800" height="600"
- 16.style="opacity: 0.1; position: absolute; top: 0; left: 0;">
- 17.</iframe>
- 18.<p>If you can see the target site inside this iframe, clickjacking is possible.</p>
- 19.</body>
- 20.</html>
- 21.
`
Phase 2: Implement Protection
- 1.Add X-Frame-Options and CSP headers in Nginx:
- 2.```nginx
- 3.server {
- 4.# Block all iframe embedding
- 5.add_header X-Frame-Options "DENY" always;
# Modern alternative using CSP (preferred) add_header Content-Security-Policy "frame-ancestors 'none';" always;
# If you need to allow specific domains to embed your site: # add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-partner.example.com;" always;
location / { proxy_pass http://backend; } } ```
- 1.Add headers in Apache:
- 2.```apache
- 3.<VirtualHost *:443>
- 4.Header always set X-Frame-Options "DENY"
- 5.Header always set Content-Security-Policy "frame-ancestors 'none'"
# For same-origin only: # Header always set X-Frame-Options "SAMEORIGIN" </VirtualHost> ```
- 1.Add headers in application code:
Express.js:
``javascript
const helmet = require('helmet');
app.use(helmet({
frameguard: { action: 'deny' },
contentSecurityPolicy: {
directives: {
frameAncestors: ["'none'"],
// Or allow specific domains:
// frameAncestors: ["'self'", "https://trusted.example.com"],
}
}
}));
Django:
``python
# settings.py
SECURE_FRAME_DENY = True # Sets X-Frame-Options: DENY
# Or for modern approach:
SECURE_CONTENT_TYPE_NOSNIFF = True
# CSP header (requires django-csp package):
CSP_FRAME_ANCESTORS = ("'none'",)
Spring Boot:
``java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.deny())
.contentSecurityPolicy(csp -> csp
.policyDirectives("frame-ancestors 'none'")
)
);
return http.build();
}
}
- 1.If legitimate iframe embedding is required, use frame busting JavaScript:
- 2.```javascript
- 3.// Add to all pages that must not be framed
- 4.if (window.self !== window.top) {
- 5.// We are in an iframe - break out
- 6.window.top.location = window.self.location;
- 7.}
// More robust version (handles sandboxed iframes): (function() { try { if (window.self !== window.top) { // Attempt to break out window.top.location = window.self.location; } } catch (e) { // Cannot access top - display overlay warning var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;' + 'background:white;z-index:999999;display:flex;align-items:center;' + 'justify-content:center;font-family:sans-serif;'; overlay.innerHTML = '<div style="text-align:center;padding:40px;">' + '<h1>This page cannot be displayed in a frame.</h1>' + '<p><a href="' + window.location.href + '" target="_top">' + 'Click here to open in a new window</a></p></div>'; document.body.appendChild(overlay); } })(); ```
Phase 3: Verify and Monitor
Prevention
- Always set
X-Frame-Options: DENYorContent-Security-Policy: frame-ancestors 'none' - Use
SAMEORIGINonly if your own pages need to iframe each other - Use
CSP frame-ancestorsinstead ofX-Frame-Optionswhen possible (more flexible) - Include security header checks in CI/CD pipeline
- Run regular security scans that check for missing security headers
- Document which endpoints (if any) legitimately need iframe embedding
- Use
X-Content-Type-Options: nosniffas a defense-in-depth measure