Clickjacking Protection Missing
What This Means
Clickjacking (also known as UI redressing) is an attack where a malicious site embeds your website in an invisible iframe and tricks users into clicking on your site's buttons or links while they think they're clicking on something else. Without proper protection (X-Frame-Options header or frame-ancestors CSP directive), attackers can overlay your site with deceptive content and hijack user actions.
How Clickjacking Works
Attack Scenario:
- Attacker creates malicious website with invisible iframe
- Iframe loads your legitimate website
- Attacker overlays enticing buttons/content over your site
- User thinks they're clicking attacker's content
- Actually clicking invisible buttons on your site
- User unknowingly performs actions (delete account, transfer money, change settings)
Example Attack:
<!-- Malicious website: evil.com -->
<html>
<body>
<h1>Click here for FREE PRIZE! π</h1>
<!-- Your legitimate site loaded invisibly -->
<iframe src="https://yourbank.com/transfer"
style="position:absolute; top:0; left:0; opacity:0.0001; width:100%; height:100%;">
</iframe>
<!-- User clicks "FREE PRIZE" but actually clicks "Confirm Transfer" on your site -->
</body>
</html>
Impact on Your Business
Security Risks:
- Account takeover - Users tricked into changing passwords, adding admin users
- Financial fraud - Unauthorized transactions, fund transfers
- Data theft - Users manipulated into sharing sensitive information
- Social engineering - Actions performed without user knowledge or consent
- Reputation damage - Your site used as attack vector
Common Targets:
- Banking/financial sites (transfer funds, add payees)
- Social media (follow accounts, like posts, share content)
- E-commerce (add items to cart, complete purchases)
- Admin panels (create accounts, change permissions)
- Email systems (send emails, forward messages)
- OAuth flows (grant permissions to malicious apps)
Compliance Impact:
- Security audit failures
- PCI DSS requirement violations
- Regulatory compliance issues
- Liability for unauthorized actions
- Customer trust erosion
How to Diagnose
Method 1: Security Headers Check
- Visit SecurityHeaders.com
- Enter your domain
- Review the report
- Look for X-Frame-Options or CSP frame-ancestors
What to Look For:
β Protected:
X-Frame-Options: DENY
or
X-Frame-Options: SAMEORIGIN
or
Content-Security-Policy: frame-ancestors 'self'
β Vulnerable:
X-Frame-Options: Not set
Content-Security-Policy: No frame-ancestors directive
Method 2: Browser DevTools
- Open your website
- Open DevTools (
F12) - Go to Network tab
- Reload page
- Click main document
- Check Headers β Response Headers
What to Look For:
β
PROTECTED:
X-Frame-Options: DENY
or
X-Frame-Options: SAMEORIGIN
or
Content-Security-Policy: frame-ancestors 'self'
β VULNERABLE:
(No X-Frame-Options header)
(No frame-ancestors in CSP)
Method 3: Manual Iframe Test
Create a simple test HTML file:
<!-- test-clickjacking.html -->
<!DOCTYPE html>
<html>
<head>
<title>Clickjacking Test</title>
</head>
<body>
<h1>Testing if site can be framed:</h1>
<iframe src="https://your-website.com" width="800" height="600"></iframe>
</body>
</html>
Open the file in browser:
β Protected (Good):
- Iframe doesn't load
- Console error: "Refused to display 'https://your-website.com' in a frame because it set 'X-Frame-Options' to 'deny'"
- Blank frame or error message
β Vulnerable (Bad):
- Website loads in iframe
- No errors in console
- Full website visible and functional in frame
Method 4: curl Command
# Check for X-Frame-Options header
curl -I https://your-website.com | grep -i x-frame-options
# Check for CSP frame-ancestors
curl -I https://your-website.com | grep -i content-security-policy
# Expected output (protected):
X-Frame-Options: DENY
# or
Content-Security-Policy: frame-ancestors 'self'
# No output = vulnerable
Method 5: Browser Console Test
// Try to load your site in an iframe programmatically
const iframe = document.createElement('iframe');
iframe.src = 'https://your-website.com';
document.body.appendChild(iframe);
// Check console for errors:
// β
Protected: "Refused to display in a frame..."
// β Vulnerable: No error, iframe loads
Method 6: Online Testing Tools
OWASP ZAP:
- Install OWASP ZAP
- Configure browser proxy
- Browse your website
- Check Alerts tab
- Look for "X-Frame-Options Header Not Set"
Qualys SSL Labs:
- Visit SSL Labs
- Enter your domain
- Review security headers section
- Check X-Frame-Options status
General Fixes
Fix 1: Add X-Frame-Options Header (Recommended)
Most compatible and simple solution:
DENY (most secure):
X-Frame-Options: DENY
- Prevents ANY site from framing your pages
- Recommended for sites that don't need to be embedded
- Highest security
SAMEORIGIN (balanced):
X-Frame-Options: SAMEORIGIN
- Only your own site can frame your pages
- Good for sites with legitimate iframes (admin panels, dashboards)
- Allows same-domain embedding
ALLOW-FROM (deprecated, don't use):
X-Frame-Options: ALLOW-FROM https://trusted.com
- β οΈ NOT supported by Chrome/Safari
- Use CSP frame-ancestors instead
Fix 2: CSP frame-ancestors Directive (Modern)
Modern, flexible approach:
Deny all framing:
Content-Security-Policy: frame-ancestors 'none'
Allow same-origin only:
Content-Security-Policy: frame-ancestors 'self'
Allow specific domains:
Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com https://partner.com
Combine with other CSP directives:
Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; script-src 'self' https://cdn.example.com
Fix 3: Web Server Configuration
Nginx:
# In nginx.conf or site config
server {
listen 443 ssl http2;
server_name example.com;
# X-Frame-Options
add_header X-Frame-Options "SAMEORIGIN" always;
# OR CSP frame-ancestors (more modern)
add_header Content-Security-Policy "frame-ancestors 'self'" always;
# OR both for defense in depth
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
# Rest of your config...
}
Apache (.htaccess):
<IfModule mod_headers.c>
# X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
# OR CSP frame-ancestors
Header always set Content-Security-Policy "frame-ancestors 'self'"
# OR both
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
</IfModule>
Apache (httpd.conf / apache2.conf):
<VirtualHost *:443>
ServerName example.com
# X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
# OR CSP frame-ancestors
Header always set Content-Security-Policy "frame-ancestors 'self'"
</VirtualHost>
IIS (web.config):
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<!-- X-Frame-Options -->
<add name="X-Frame-Options" value="SAMEORIGIN" />
<!-- OR CSP frame-ancestors -->
<add name="Content-Security-Policy" value="frame-ancestors 'self'" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Fix 4: Application-Level Configuration
Node.js/Express (using Helmet):
const express = require('express');
const helmet = require('helmet');
const app = express();
// Option 1: Use Helmet's frameguard (X-Frame-Options)
app.use(helmet.frameguard({ action: 'sameorigin' }));
// or
app.use(helmet.frameguard({ action: 'deny' }));
// Option 2: Use CSP frame-ancestors
app.use(helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'self'"],
// other CSP directives...
}
}));
// Option 3: Both for maximum compatibility
app.use(helmet.frameguard({ action: 'sameorigin' }));
app.use(helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'self'"],
}
}));
Node.js (manual):
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
next();
});
PHP:
<?php
// Add to top of PHP files or in a common header file
header("X-Frame-Options: SAMEORIGIN");
header("Content-Security-Policy: frame-ancestors 'self'");
?>
Python/Flask:
from flask import Flask
app = Flask(__name__)
@app.after_request
def set_security_headers(response):
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
return response
Python/Django:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ... other middleware
]
# Security settings
X_FRAME_OPTIONS = 'SAMEORIGIN' # or 'DENY'
# OR use CSP
CSP_FRAME_ANCESTORS = ("'self'",)
Ruby/Rails:
# config/application.rb
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN'
}
# OR in controller
class ApplicationController < ActionController::Base
before_action :set_security_headers
def set_security_headers
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
end
end
ASP.NET Core:
// Startup.cs
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'self'");
await next();
});
// OR use built-in middleware
app.UseXFrameOptions(XFrameOptionsPolicy.SameOrigin);
}
Java/Spring Boot:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin() // X-Frame-Options: SAMEORIGIN
.contentSecurityPolicy("frame-ancestors 'self'"); // CSP
}
}
Fix 5: CDN Configuration
Cloudflare:
- Log into Cloudflare Dashboard
- Select your domain
- Go to Rules β Transform Rules β HTTP Response Header Modification
- Create new rule:
- Field: X-Frame-Options
- Action: Set static
- Value: SAMEORIGIN
OR use Cloudflare Workers:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const response = await fetch(request)
const newHeaders = new Headers(response.headers)
newHeaders.set('X-Frame-Options', 'SAMEORIGIN')
newHeaders.set('Content-Security-Policy', "frame-ancestors 'self'")
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
})
}
Netlify (netlify.toml):
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "SAMEORIGIN"
Content-Security-Policy = "frame-ancestors 'self'"
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "Content-Security-Policy",
"value": "frame-ancestors 'self'"
}
]
}
]
}
Fix 6: Allow Specific Embedders
If you need to allow specific trusted sites to embed your content:
CSP frame-ancestors (recommended):
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com https://another-trusted.com
Application logic:
// Node.js example - dynamic X-Frame-Options based on referrer
app.use((req, res, next) => {
const trustedDomains = [
'https://trusted-partner.com',
'https://another-trusted.com'
];
const referrer = req.get('Referer') || '';
const isTrusted = trustedDomains.some(domain => referrer.startsWith(domain));
if (isTrusted) {
res.setHeader('X-Frame-Options', 'ALLOW-FROM ' + referrer);
// Note: ALLOW-FROM is deprecated, use CSP instead
} else {
res.setHeader('X-Frame-Options', 'DENY');
}
// Better: Use CSP
const frameAncestors = isTrusted
? `frame-ancestors 'self' ${trustedDomains.join(' ')}`
: "frame-ancestors 'none'";
res.setHeader('Content-Security-Policy', frameAncestors);
next();
});
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing clickjacking protection:
Test 1: HTTP Header Check
# Check X-Frame-Options
curl -I https://your-website.com | grep -i x-frame-options
# Expected: X-Frame-Options: SAMEORIGIN (or DENY)
# Check CSP frame-ancestors
curl -I https://your-website.com | grep -i content-security-policy
# Expected: Content-Security-Policy: frame-ancestors 'self'
Test 2: Browser DevTools
- Open your website
- DevTools β Network β Reload
- Click main document
- Headers tab β Response Headers
- Verify presence of X-Frame-Options or CSP frame-ancestors
Test 3: Iframe Test
<!-- Create test file: test.html -->
<!DOCTYPE html>
<html>
<body>
<h1>Clickjacking Protection Test</h1>
<iframe src="https://your-website.com" width="800" height="600"></iframe>
<p>If your site loads above, protection is NOT working.</p>
<p>If you see an error or blank frame, protection is working! β
</p>
</body>
</html>
Expected result:
- Console error: "Refused to display in a frame"
- Blank iframe
- Site does not load
Test 4: SecurityHeaders.com
- Visit SecurityHeaders.com
- Enter your domain
- Check for:
- X-Frame-Options present
- Or frame-ancestors in CSP
- Green checkmark β
- Improved security grade
Test 5: Multiple Pages
Test on various pages:
- Homepage
- Login page
- Admin panel
- API endpoints
- Static assets
All should have protection headers.
Common Mistakes
- Using ALLOW-FROM - Deprecated, not supported in modern browsers
- Only protecting homepage - Apply to all pages and endpoints
- Conflicting headers - X-Frame-Options and CSP frame-ancestors conflict
- Not testing in production - Development environment differs from production
- Forgetting about subdomains - Each subdomain needs protection
- Breaking legitimate embeds - Need to allow specific partners
- Missing on API endpoints - API responses should also have protection
- Not handling errors - Users should know why iframe failed
- Cache issues - Old headers cached by browser/CDN
- Third-party widgets - May need to allow specific domains
Advanced Topics
Conditional Framing
Allow framing for specific pages only:
# Nginx example
location / {
add_header X-Frame-Options "DENY" always;
}
location /embed/ {
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /widget/ {
add_header Content-Security-Policy "frame-ancestors 'self' https://trusted.com" always;
}
Frame-Busting JavaScript (Deprecated)
β οΈ NOT RECOMMENDED - Can be bypassed, use headers instead:
// Old approach - DO NOT USE
if (top !== self) {
top.location = self.location;
}
Why it's bad:
- Can be bypassed with sandbox attribute
- Doesn't work with CSP
- JavaScript can be disabled
- Not reliable security
Defense in Depth
Combine multiple protections:
# Multiple layers of protection
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'; default-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Monitoring and Alerts
CSP Reporting:
Content-Security-Policy: frame-ancestors 'self'; report-uri /csp-report
Server-side logging:
app.post('/csp-report', (req, res) => {
console.warn('CSP Violation:', req.body);
// Log to monitoring system
res.status(204).end();
});
Choosing Between X-Frame-Options and CSP
Use X-Frame-Options when:
- Maximum browser compatibility needed
- Simple DENY or SAMEORIGIN sufficient
- Not using other CSP directives
- Legacy browser support required
Use CSP frame-ancestors when:
- Need to allow multiple specific domains
- Already using Content-Security-Policy
- Want more granular control
- Modern browsers only
Use both when:
- Maximum compatibility + modern features
- Defense in depth approach
- Supporting wide range of browsers
- No conflicts (ensure values match)
Example - Both:
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'