Clickjacking Protection
Introduction
Clickjacking, also known as a UI redress attack, tricks users into clicking on something different from what they perceive. An attacker embeds a target page in a transparent iframe overlaid on a decoy interface. When the user clicks a visible button, they actually interact with the hidden target page — potentially authorizing a transaction, changing settings, or granting permissions.
How Clickjacking Works
A clickjacking exploit involves three elements:
iframe {
position: absolute;
top: 50px;
left: 100px;
opacity: 0.001; /* Nearly invisible */
z-index: 10;
width: 500px;
height: 500px;
}
.decoy-button {
position: absolute;
top: 200px;
left: 150px;
z-index: 1;
}
The user sees a game or prize button but actually clicks the bank's transfer confirmation. With precise CSS positioning, the attacker makes the real button overlap the decoy.
X-Frame-Options
X-Frame-Options is a response header that controls whether a page can be displayed in an iframe.
# Deny all framing
add_header X-Frame-Options "DENY" always;
# Allow only same-origin framing
add_header X-Frame-Options "SAMEORIGIN" always;
# Flask example: set X-Frame-Options
from flask import Flask, make_response
app = Flask(__name__)
@app.after_request
def set_frame_options(response):
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
return response
# For sensitive pages (banking, admin panels), use DENY
@app.route('/admin/transfer')
def admin_transfer():
response = make_response(render_template('transfer.html'))
response.headers['X-Frame-Options'] = 'DENY'
return response
X-Frame-Options has three values: `DENY` (no framing ever), `SAMEORIGIN` (same-origin only), and `ALLOW-FROM` (deprecated, not supported in modern browsers).
CSP frame-ancestors
Content Security Policy's `frame-ancestors` directive supersedes X-Frame-Options with more granular control. It specifies which origins are allowed to embed the page.
# Allow only same-origin
add_header Content-Security-Policy "frame-ancestors 'self'" always;
# Allow specific origins
add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-app.example.com" always;
# Allow none (equivalent to DENY)
add_header Content-Security-Policy "frame-ancestors 'none'" always;
# Allow multiple specific origins
add_header Content-Security-Policy "frame-ancestors https://app1.example.com https://app2.example.com" always;
When both X-Frame-Options and CSP `frame-ancestors` are present, browsers honor the more restrictive policy. CSP `frame-ancestors` is preferred because it supports multiple origins.
# Python/Flask CSP middleware
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
Talisman(app,
content_security_policy={
'frame-ancestors': ["'self'", "https://dashboard.example.com"]
}
)
Framebusting JavaScript
Client-side framebusting detects if the page is loaded in an iframe and breaks out. However, JavaScript-based protection can be bypassed.
// Basic framebuster
if (top !== self) {
top.location = self.location;
}
// Robust framebuster with null check
if (top.location !== self.location) {
// In some browsers, accessing top.location throws SecurityError
try {
top.location.href = self.location.href;
} catch (e) {
// If blocked by cross-origin policy, still possible to break out
top.location = self.location;
}
}
// Prevention of iframe-based clickjacking with style override
if (self === top) {
document.getElementById('anti-clickjack').remove();
} else {
top.location = self.location;
}
Framebusting limitations:
* `sandbox` attribute on iframes can prevent `top.location` modification
* `noopener` links bypass frame busters
* Attackers can use `onbeforeunload` to prevent navigation
Testing for Clickjacking Vulnerabilities
# Test with curl
curl -I https://target-website.com/admin | grep -i "x-frame-options\|content-security-policy"
# Check for frame-ancestors specifically
curl -sI https://target-website.com | \
grep -E 'X-Frame-Options|frame-ancestors' || \
echo "NO PROTECTION DETECTED"
# Python automated check
python3 -c "
import requests
urls = [
'https://target.com/login',
'https://target.com/transfer',
'https://target.com/admin',
]
for url in urls:
resp = requests.get(url)
xfo = resp.headers.get('X-Frame-Options', 'MISSING')
csp = resp.headers.get('Content-Security-Policy', '')
fa = 'frame-ancestors' in csp or 'frame-ancestors' not in csp
if 'frame-ancestors' in csp:
print(f'[PROTECTED] {url}: CSP frame-ancestors')
elif xfo in ('DENY', 'SAMEORIGIN'):
print(f'[PROTECTED] {url}: X-Frame-Options: {xfo}')
else:
print(f'[VULNERABLE] {url}: No framing protection')
"
Comprehensive Protection Strategy
# Nginx: full clickjacking protection
server {
# X-Frame-Options as fallback
add_header X-Frame-Options "SAMEORIGIN" always;
# CSP frame-ancestors as modern replacement
add_header Content-Security-Policy "frame-ancestors 'self'" always;
location /admin/ {
# Stricter for sensitive areas
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
}
location /api/ {
# APIs should never be framed
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
}
}
Conclusion
Clickjacking is one of the easiest vulnerabilities to prevent but remains surprisingly common. Set `X-Frame-Options: DENY` or `SAMEORIGIN` on every response, and use CSP `frame-ancestors` for fine-grained control. For the strongest protection, deploy both headers, and always test new pages before deployment.