Clickjacking and UI Redress Attacks: Testing and Prevention

Clickjacking is one of those vulnerabilities that looks trivially simple on paper — embed a target page in an invisible iframe, position it over a decoy — and yet keeps appearing in security audits of production applications years after the defenses became well-known. It's not that developers don't know about X-Frame-Options. It's that "we have security headers" gets assumed without verification, misconfigurations creep in via new endpoints, and some attack variants bypass header-based defenses entirely.

This guide covers the full attack surface: basic clickjacking, multi-step attacks, drag-and-drop variants, cursor manipulation, nested frame bypasses, and the testing methodology you need to find them. Then the remediation — what actually works and what only looks like it works.

The Basic Attack

Classic clickjacking is a UI redress attack — the attacker redraws the user interface to deceive the victim about what they're actually clicking. The mechanism:

  1. The attacker creates a page with a compelling reason to click (a prize, a captcha, a button)
  2. The target application's page is loaded in an <iframe> overlaid on the decoy
  3. The iframe is made transparent (opacity: 0.001 — zero opacity can be browser-optimized away)
  4. The iframe is positioned so that a sensitive action (delete account, confirm payment, change email) aligns with the decoy's click target
  5. The victim clicks what they think is the decoy element but actually activates the target application's action

The critical detail: the iframe loads the target application with the victim's cookies. If the victim is logged into the target application, the iframe has their authenticated session. The action they unknowingly click is executed in their authenticated context.

<!-- Attacker's page -->
<style>
    .decoy { position: absolute; top: 200px; left: 300px; z-index: 1; }
    .target-iframe {
        position: absolute; top: 0; left: 0;
        width: 100%; height: 100%;
        opacity: 0.001;  /* Nearly invisible, but renders and accepts clicks */
        z-index: 2;      /* Above decoy — receives clicks first */
    }
</style>

<div class="decoy">
    <button>Click here to claim your prize!</button>
</div>

<iframe class="target-iframe" src="https://vulnerable-app.com/account/delete"></iframe>

When the victim clicks "claim your prize," they actually click the invisible "Delete Account" button in the iframe.

Attack Variants

Multi-Step Clickjacking

Single-click attacks are limited to actions that complete in one click. Multi-step clickjacking sequences multiple invisible clicks to complete a multi-step flow — for example, navigating to a settings page and then confirming a destructive action. The attacker creates a sequence of decoy interactions, each aligned with a target click in the iframe, and steps through them as the victim interacts.

Multi-step attacks require more precise alignment and are more fragile, but they unlock a wider range of target actions that include confirmation dialogs.

Drag-and-Drop Clickjacking

Drag-and-drop attacks exploit text selection and drag operations rather than clicks. The attack tricks the victim into selecting text from the attacker's page via a drag operation, but the drag-end event fires on the iframe's input field, typing the attacker's text into it. This can populate form fields the victim never intended to fill — such as an email address in an "add email to account" field.

This attack bypasses click-based defenses because no click event occurs. The victim genuinely performed a drag operation on what they thought was the attacker's page; the drag-end landed on the invisible iframe.

Cursor Redress

A sophisticated variant uses CSS cursor manipulation to show the victim a fake cursor positioned differently from the real one. The attacker's page sets cursor: none and renders a fake cursor image that appears offset from the real cursor position. The victim moves their real cursor to what they believe is a safe location (the fake cursor shows it near a benign element), while the real cursor is positioned over a sensitive element in the iframe.

Modern browser security updates have reduced this attack's effectiveness on desktop — browsers constrain custom cursor offset distance — but it illustrates how UI redress extends beyond transparency tricks.

Likejacking and Shareejacking

Social media action buttons (Like, Share, Follow) embedded via official SDK iframes are common clickjacking targets. The attack positions the Like button iframe over a decoy and tricks authenticated users into liking or sharing content they never intended to engage with. While less severe than account takeover, likejacking has been weaponized for social spam at scale.

Nested Frame Bypasses

Some framing defenses use JavaScript frame-busting instead of HTTP headers. Frame-busting scripts typically check window.top !== window.self and redirect if framed. These JavaScript-based defenses can be bypassed.

Sandbox Bypass

The sandbox attribute on <iframe> restricts what the framed content can do. Setting sandbox without allow-scripts prevents the frame-busting JavaScript from running, while the page still loads and accepts clicks:

<iframe sandbox="allow-forms allow-same-origin"
        src="https://vulnerable-app.com/account/delete">
</iframe>

Without allow-scripts, the frame-busting script never executes. The page renders, the victim clicks the hidden button, and the form submission (allowed by allow-forms) goes through. JavaScript-based frame-busting provides no protection when the framing page can sandbox the iframe.

Double-Frame Bypass

Some frame-busting scripts check only direct parent framing. Double-framing — embedding the target in an intermediate iframe, then embedding that intermediate in the outer page — bypasses checks that only look one level up. The target page sees a same-origin parent (the intermediate iframe's domain) and doesn't bust the frame, while the outer page is attacker-controlled.

Testing Methodology

Step 1: Check Response Headers

Start with a header check across all significant application endpoints:

curl -sI https://app.example.com/account/settings | grep -i "x-frame\|content-security"

You're looking for one of:

Header Value Effect
X-Frame-Options DENY Blocks all framing
X-Frame-Options SAMEORIGIN Allows same-origin framing only
X-Frame-Options ALLOW-FROM uri Allows specified origin (deprecated, limited support)
Content-Security-Policy frame-ancestors 'none' Blocks all framing
Content-Security-Policy frame-ancestors 'self' Allows same-origin framing only

Neither header present means the application is frameable from any origin. That's the starting point for a clickjacking test.

Step 2: Confirm Frameability

Headers can be set on some responses and not others. Check endpoints that perform sensitive actions specifically — account settings, payment confirmations, delete actions, permission changes — not just the homepage:

#!/bin/bash
# Check framing headers across sensitive endpoints
ENDPOINTS=(
    "https://app.example.com/account/settings"
    "https://app.example.com/account/delete"
    "https://app.example.com/payment/confirm"
    "https://app.example.com/admin/users"
)

for url in "${ENDPOINTS[@]}"; do
    echo "=== $url ==="
    curl -sI "$url" | grep -iE "x-frame-options|content-security-policy"
    echo ""
done

Also test unauthenticated requests — some applications only set framing headers for authenticated sessions, leaving the login page frameable (which enables credential-harvesting attacks via a framed login).

Step 3: Build a Proof-of-Concept Frame

To confirm exploitability, build a minimal PoC. Open your browser's developer tools and create an iframe in the console:

// In browser console on an attacker-controlled page:
const iframe = document.createElement('iframe');
iframe.src = 'https://target-app.com/account/settings';
iframe.style.cssText = 'width:100%;height:600px;border:0;';
document.body.appendChild(iframe);

If the iframe loads the target application, the application is frameable. If the browser refuses due to X-Frame-Options or frame-ancestors, the defense is working. Check the browser console for the specific error message — Refused to display in a frame because it set 'X-Frame-Options' to 'deny' versus a CSP error.

Step 4: Test with Authentication

Clickjacking is only exploitable if the victim is authenticated. Test with an authenticated session to verify that the framed content includes the authenticated state (account details visible, forms pre-populated with the user's data) and that sensitive actions are present and accessible in the framed page.

Step 5: Verify Frame-Busting Scripts

If the application uses JavaScript frame-busting rather than HTTP headers, test the sandbox bypass:

<!-- Test if sandbox disables frame-busting -->
<iframe sandbox="allow-forms allow-same-origin"
        src="https://target-app.com/account/delete"
        style="width:800px;height:600px;">
</iframe>

If the page loads and renders normally in the sandboxed iframe (no frame-bust redirect), the JavaScript defense is bypassed.

When SAMEORIGIN Isn't Enough

X-Frame-Options: SAMEORIGIN and frame-ancestors 'self' permit framing by same-origin pages. If the application has any page that allows arbitrary HTML injection — a stored XSS, a user-controlled page on the same domain, an open HTML redirect — a same-origin frame-ancestor restriction doesn't prevent clickjacking. The attacker frames the target from the injection point on the same origin.

Check for:

Reporting Severity

Clickjacking severity depends on what the framed action achieves. Context matters:

Target Action Severity
Delete account / irreversible data deletion High
Change email address / password reset trigger High — path to account takeover
Authorize OAuth application with broad scopes High
Transfer funds / confirm payment Critical depending on value
Add admin user / escalate privilege Critical
Like/share on social media Low-Medium
Subscribe to newsletter Informational

Strictly informational "frameable homepage with no sensitive actions visible" should be reported as a best-practice finding, not a critical vulnerability. The finding requires both: a frameable page AND a sensitive action accessible in that frame.

Remediation

CSP frame-ancestors: The Modern Fix

Content-Security-Policy: frame-ancestors 'none' is the current recommended defense. It has broader browser support than X-Frame-Options, works with non-HTTP frames (SVG, object), and is not subject to the ALLOW-FROM deprecation issues that affect older XFO implementations.

# Nginx: add to server or location block
add_header Content-Security-Policy "frame-ancestors 'none';" always;

# Apache: .htaccess or VirtualHost
Header always set Content-Security-Policy "frame-ancestors 'none';"

# Express.js middleware
app.use((req, res, next) => {
    res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
    next();
});

The always flag in Nginx/Apache is critical — without it, the header is only set on 200 responses, leaving error pages frameable.

X-Frame-Options: The Legacy Complement

Set both Content-Security-Policy: frame-ancestors and X-Frame-Options: DENY. CSP is ignored by IE11 and very old browsers; XFO provides a fallback. Modern browsers that support both will use CSP. The redundancy costs nothing and adds compatibility coverage.

add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none';" always;

SameSite Cookies: Defense in Depth

Setting SameSite=Strict or SameSite=Lax on session cookies means the cookies are not sent when the page is loaded in an iframe from a cross-site context. Without the session cookie, the framed page loads unauthenticated, and no authenticated actions are available for the attacker to exploit.

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

This doesn't prevent framing (the page still loads) but eliminates the attack's impact — a framed unauthenticated page can't perform any privileged actions. Combined with framing restrictions, it's defense in depth. Note: SameSite=Lax still sends cookies on top-level navigations (normal links), which preserves usability while blocking cross-site iframe embedding.

Token-Based CSRF Defenses: Not a Clickjacking Defense

CSRF tokens don't prevent clickjacking. The token is embedded in the page that loads in the iframe, and the victim's click submits the form including the valid token. Clickjacking bypasses CSRF defenses because the request originates from the victim's browser with the correct token — the victim's session cookies and the CSRF token are both present. The victim's action is "legitimate" from the application's perspective; they just didn't intend to perform it.

Ironimo checks all application endpoints for missing X-Frame-Options and frame-ancestors CSP directives, identifies frameable pages with sensitive actions, and tests sandbox-based frame-busting bypasses.

Start free scan

← Back to Blog