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:
Click here for a free prize!
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
Framebusting limitations:
-
sandboxattribute on iframes can preventtop.locationmodification -
noopenerlinks bypass frame busters -
Attackers can use
onbeforeunloadto 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.
Enjoy this article? Share your thoughts, questions, or experiences in the comments below — your insights help other readers too.
Join the discussion ↓