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-Options header:
  • `
  • [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-Options header
  • Content-Security-Policy header missing frame-ancestors directive
  • 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. 1.Confirm missing headers:
  2. 2.```bash
  3. 3.curl -sI https://your-app.example.com/
  4. 4.# Check for:
  5. 5.# X-Frame-Options: DENY or SAMEORIGIN
  6. 6.# Content-Security-Policy: frame-ancestors 'none' or 'self'
  7. 7.`
  8. 8.Test iframe embedding:
  9. 9.```html
  10. 10.<!-- Save as test-clickjacking.html and open in browser -->
  11. 11.<html>
  12. 12.<body>
  13. 13.<h1>Clickjacking Test</h1>
  14. 14.<iframe src="https://your-app.example.com/login"
  15. 15.width="800" height="600"
  16. 16.style="opacity: 0.1; position: absolute; top: 0; left: 0;">
  17. 17.</iframe>
  18. 18.<p>If you can see the target site inside this iframe, clickjacking is possible.</p>
  19. 19.</body>
  20. 20.</html>
  21. 21.`

Phase 2: Implement Protection

  1. 1.Add X-Frame-Options and CSP headers in Nginx:
  2. 2.```nginx
  3. 3.server {
  4. 4.# Block all iframe embedding
  5. 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. 1.Add headers in Apache:
  2. 2.```apache
  3. 3.<VirtualHost *:443>
  4. 4.Header always set X-Frame-Options "DENY"
  5. 5.Header always set Content-Security-Policy "frame-ancestors 'none'"

# For same-origin only: # Header always set X-Frame-Options "SAMEORIGIN" </VirtualHost> ```

  1. 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. 1.If legitimate iframe embedding is required, use frame busting JavaScript:
  2. 2.```javascript
  3. 3.// Add to all pages that must not be framed
  4. 4.if (window.self !== window.top) {
  5. 5.// We are in an iframe - break out
  6. 6.window.top.location = window.self.location;
  7. 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: DENY or Content-Security-Policy: frame-ancestors 'none'
  • Use SAMEORIGIN only if your own pages need to iframe each other
  • Use CSP frame-ancestors instead of X-Frame-Options when 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: nosniff as a defense-in-depth measure