Documentation

# 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.

Clickjacking Illustration
Clickjacking Illustration

# 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


# 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

[Attacker's Malicious Page]
<!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>
[What User Sees]
Win a Free iPhone!
[Click Here to Claim Your Prize!]  ← Visible button
[What Actually Happens]
[Invisible Bank Transfer Form]      ← Actually clicking here
  Transfer to: attacker@evil.com
  Amount: $1000
  [Confirm Transfer] ← This is what gets clicked

# 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

[Attack Setup]
<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>
[Attack Result]
User clicks "CLICK TO CLAIM" at (450, 250)
↓
Actually clicks hidden "Confirm Transfer" at (450, 250)
↓
$5,000 transferred to attacker!

# 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

[Attacker's Page]
<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>
[JavaScript Handler]
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>

# 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

[Exploit Code]
<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>
[Attack Flow]
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)

[Apache .htaccess]
# 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 Configuration]
# Deny all framing
add_header X-Frame-Options "DENY" always;

# Or allow only same origin
add_header X-Frame-Options "SAMEORIGIN" always;
[Express.js (Node.js)]
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]
<?php
// Deny all framing
header('X-Frame-Options: DENY');

// Or allow only same origin
header('X-Frame-Options: SAMEORIGIN');
?>
[Django (Python)]
# settings.py
X_FRAME_OPTIONS = 'DENY'
# or
X_FRAME_OPTIONS = 'SAMEORIGIN'
[Rails (Ruby)]
# config/application.rb
config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN'
}
[Java Servlet]
response.addHeader("X-Frame-Options", "DENY");
// or
response.addHeader("X-Frame-Options", "SAMEORIGIN");
[ASP.NET (C#)]
// Web.config
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="X-Frame-Options" value="DENY" />
    </customHeaders>
  </httpProtocol>
</system.webServer>
  • 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"
[Nginx]
add_header Content-Security-Policy "frame-ancestors 'none'" always;
# or
add_header Content-Security-Policy "frame-ancestors 'self'" always;
[Express.js]
const 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'");
?>
[Django]
# settings.py
CSP_FRAME_ANCESTORS = ["'none'"]
# or
CSP_FRAME_ANCESTORS = ["'self'"]
# or
CSP_FRAME_ANCESTORS = ["'self'", "https://trusted-site.com"]
  • 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)

[Basic Frame Buster]
// Detect if page is in iframe
if (window.top !== window.self) {
    // Break out of iframe
    window.top.location = window.self.location;
}
[Enhanced Frame Buster]
// 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;
        }
    }
})();
[Body Style Protection]
<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

[Express.js]
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]
<?php
session_set_cookie_params([
    'samesite' => 'Strict',
    'secure' => true,
    'httponly' => true
]);

setcookie('session_id', $value, [
    'samesite' => 'Strict',
    'secure' => true,
    'httponly' => true
]);
?>
[Django]
# settings.py
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_SECURE = True
[Java Servlet]
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

[Flask Example - Action Validation]
@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'
[Node.js - Two-Step Confirmation]
// 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();
        }
    }
}
[CAPTCHA Protection]
<!-- 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

[Check X-Frame-Options]
curl -I https://target-site.com | grep -i "x-frame-options"

# Expected for protected site:
# X-Frame-Options: DENY
# or
# X-Frame-Options: SAMEORIGIN
[Check CSP Headers]
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'
[Full Header Check]
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-Options
  • Content-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
  1. Configure browser proxy
  2. Browse target site
  3. Check Proxy → HTTP History
  4. 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

# 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

# 4. Forgetting Error Pages


# Advanced Defense Strategies

# 1. UI Confirmation Patterns

[Visual Security Indicators]
// 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();
}
[Security Border]
/* 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

[Track User Interaction Patterns]
// 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
    });
});
[Prevent Overlays]
// 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-Options header to DENY or SAMEORIGIN
  • Set Content-Security-Policy: frame-ancestors directive
  • Configure both headers for maximum browser compatibility
  • Test headers are present in all responses (including error pages)
  • Set SameSite=Strict or SameSite=Lax on session cookies
  • Enable HTTPS for all pages (Secure cookie 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 Origin and Referer headers 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


# References & Resources

# Official Documentation

# Testing Tools

# Learning Resources


# 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