#
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
┌─────────────────────────┐
│ WIN FREE iPHONE! │
│ │
│ [Click Here to Win] │ ← You see this
│ │
└─────────────────────────┘
┌─────────────────────────┐
│ 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
<!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>
Win a Free iPhone!
[Click Here to Claim Your Prize!] ← Visible button
[Invisible Bank Transfer Form] ← Actually clicking here
Transfer to: attacker@evil.com
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
<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>
User 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
<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
Trick user into granting webcam/microphone permissions
<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>
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
<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>
function 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
<!-- 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
<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)
<!-- 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
<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>
1. 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
<!-- 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)
# 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"
# Deny all framing
add_header X-Frame-Options "DENY" always;
# Or allow only same origin
add_header X-Frame-Options "SAMEORIGIN" always;
const helmet = require('helmet');
// Deny all framing
app.use(helmet.frameguard({ action: 'deny' }));
// Or allow same origin only
app.use(helmet.frameguard({ action: 'sameorigin' }));
<?php
// Deny all framing
header('X-Frame-Options: DENY');
// Or allow only same origin
header('X-Frame-Options: SAMEORIGIN');
?>
# settings.py
X_FRAME_OPTIONS = 'DENY'
# or
X_FRAME_OPTIONS = 'SAMEORIGIN'
# config/application.rb
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN'
}
response.addHeader("X-Frame-Options", "DENY");
// or
response.addHeader("X-Frame-Options", "SAMEORIGIN");
// 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)
# 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"
add_header Content-Security-Policy "frame-ancestors 'none'" always;
# or
add_header Content-Security-Policy "frame-ancestors 'self'" always;
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'none'"] // Most secure
// or: frameAncestors: ["'self'"]
// or: frameAncestors: ["'self'", "https://trusted-site.com"]
}
}));
<?php
header("Content-Security-Policy: frame-ancestors 'none'");
// or
header("Content-Security-Policy: frame-ancestors 'self'");
?>
# 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
@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.
// Detect if page is in iframe
if (window.top !== window.self) {
// Break out of iframe
window.top.location = window.self.location;
}
// 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;
}
}
})();
<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:
<!-- 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
app.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
session_set_cookie_params([
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);
setcookie('session_id', $value, [
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);
?>
# settings.py
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_SECURE = True
Cookie 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)
<!-- 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
@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'
// 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();
}
}
}
<!-- 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
<!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
curl -I https://target-site.com | grep -i "x-frame-options"
# Expected for protected site:
# X-Frame-Options: DENY
# or
# X-Frame-Options: SAMEORIGIN
curl -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'
curl -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
if (window.top === window.self) {
console.log("Not framed - test with iframe");
} else {
console.log("Currently in frame");
}
#
Automated Testing Tools
# Install OWASP ZAP
# Run passive scan
zap-cli quick-scan https://target-site.com
# Check for clickjacking vulnerabilities in report
- Configure browser proxy
- Browse target site
- Check Proxy → HTTP History
- Look for missing X-Frame-Options headers
nikto -h https://target-site.com -Tuning 7
# Output will show if X-Frame-Options is missing
- securityheaders.com - Checks for clickjacking protection headers
- clickjacker.io - Automated clickjacking test
#
Common Mistakes
#
1. Only Using Client-Side Protection
Wrong Approach
// This alone is NOT sufficient
if (window.top !== window.self) {
window.top.location = window.self.location;
}
Correct Approach
# 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
# Only set on homepage
<Location "/">
Header set X-Frame-Options "DENY"
</Location>
# Set on ALL responses (note: "always")
Header always set X-Frame-Options "DENY"
#
3. Using ALLOW-FROM with Multiple Domains
Wrong
# ALLOW-FROM only supports ONE domain
Header set X-Frame-Options "ALLOW-FROM https://site1.com https://site2.com"
Correct
# 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
# Use "always" to include error responses
Header always set X-Frame-Options "SAMEORIGIN"
# Headers apply to all responses by default
add_header X-Frame-Options "SAMEORIGIN" always;
#
Advanced Defense Strategies
#
1. UI Confirmation Patterns
// 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();
}
/* 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
// 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
});
});
// 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 Guardian Proxy 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