Clickjacking — also known as UI redressing — is an attack in which the victim thinks they are clicking one thing but is actually clicking another. The attacker loads the legitimate application inside an invisible iframe, places that iframe precisely over an attractive element on their own page (a “Win a prize” button, for example), and captures the click. As far as the browser is concerned, the click is entirely legitimate: it happens inside the victim’s authenticated session, with their cookies, on the real origin of the application.

The result is that the user performs a sensitive action — changing the recovery email, authorizing a payment, granting an OAuth app, deleting the account — without ever intending to.

How it actually works

The attack relies on three ingredients:

  1. The target application allows itself to be loaded in an iframe (it does not send headers forbidding it).
  2. The victim has an active session on the target application.
  3. The attacker can lure the victim to a page they control.

The malicious page mounts the victim’s iframe over its bait and makes it transparent:

<style>
  /* The victim's iframe sits on top, invisible, capturing the real click */
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 800px;
    height: 600px;
    opacity: 0.0;        /* fully transparent */
    z-index: 2;
  }
  /* The bait sits underneath, visible, aligned with the target's real button */
  #lure {
    position: absolute;
    top: 410px;          /* position of the "Confirm" button inside the iframe */
    left: 300px;
    z-index: 1;
  }
</style>

<button id="lure">🎁 Click to claim your prize</button>
<iframe src="https://app.example.com/settings/delete-account"></iframe>

The user sees the prize button, clicks it, but the click passes through to the “Confirm deletion” button of the transparent iframe. While building the attack, the adversary inverts the logic (opacity: 0.5) to align the bait with pixel precision, then sets opacity back to zero.

Variations beyond the simple click

  • Likejacking: hijacking clicks on social “like”/“follow” buttons to inflate engagement.
  • Cursorjacking: the attacker hides the real cursor and draws a fake, offset one, making the victim click somewhere different from what they see.
  • Drag-and-drop / fill jacking: by combining iframes and drag events, an attacker can induce the victim to fill in and drag sensitive data into attacker-controlled fields.
  • Double clickjacking: a more recent technique that abuses the timing window between two clicks to bypass protections that depend on user interaction (such as OAuth consent screens).

Why JavaScript “frame busting” is not enough

For years, the default defense was frame busting: a script that detects being inside a frame and breaks out.

// Classic frame busting — fragile, do NOT rely on this as your only defense
if (top !== self) {
  top.location = self.location;
}

This pattern fails in several ways:

  • The iframe sandbox attribute can block top navigation (sandbox="allow-forms allow-scripts" without allow-top-navigation), neutralizing top.location.
  • Old browsers and onbeforeunload handlers can cancel the redirect.
  • There are exploitable race conditions between script load and the click.

Frame busting is, at best, an extra layer — never the primary defense.

The correct defense: response headers

The solid protection against clickjacking is to tell the browser, from the server, who may (or may not) embed your pages.

The Content Security Policy frame-ancestors directive is the current standard and the most flexible. To forbid embedding entirely:

Content-Security-Policy: frame-ancestors 'none';

To allow only the same origin (and no third parties):

Content-Security-Policy: frame-ancestors 'self';

To allow a specific partner:

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;

frame-ancestors supports multiple origins, scheme and port wildcards, and is honored by all modern browsers. It is the only option that covers allowlist scenarios with more than one domain. To check how your site responds today, paste the headers into the Security Headers Analyzer and see the grade and the per-stack fix.

2. X-Frame-Options (legacy, still useful)

An older header with only two practical values:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

The ALLOW-FROM uri value is deprecated and ignored by most browsers. If you need an allowlist, use frame-ancestors.

Keep X-Frame-Options for compatibility with old user agents, but treat frame-ancestors as the source of truth. When the two disagree, modern browsers honor the CSP.

3. SameSite cookies on sensitive actions

Even with embedding blocked, set session cookies as SameSite=Lax (or Strict for critical areas). This ensures that, in a third-party context, the cookie does not accompany state-changing requests — a layer that also reinforces CSRF protection.

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax

4. Explicit confirmation for irreversible actions

For very high-impact operations (deleting an account, transferring funds), require a confirmation that cannot be “clicked by accident”: re-authentication, typing a word, or a challenge that the invisible overlay cannot reproduce.

How we test clickjacking in a pentest

In our testing process, verification follows an objective path:

  1. Map state-changing endpoints reachable with a single click (POST forms, toggles, consent flows).
  2. Inspect response headers looking for Content-Security-Policy: frame-ancestors and/or X-Frame-Options. Absence on any sensitive route is already a finding.
  3. Build a real PoC: embed the page in a controlled iframe and confirm it renders and is operable.
  4. Assess the impact by combining it with the sensitive function — clickjacking that only “likes” is low risk; clickjacking over “authorize OAuth app” is high.

The most common flaw we find is not the total absence of protection, but inconsistent protection: the home page and login send the headers, but the admin panel or an embeddable API endpoint does not.

Mitigation checklist

  • Content-Security-Policy: frame-ancestors 'none' (or an explicit allowlist) on every response, including APIs and authenticated pages.
  • X-Frame-Options: DENY/SAMEORIGIN as reinforcement for legacy browsers.
  • Session cookies with HttpOnly; Secure; SameSite=Lax (or Strict).
  • Strong re-authentication/confirmation on irreversible actions.
  • Headers applied centrally (middleware/edge), not route by route.
  • Frame busting only as an extra layer, never the sole defense.

Clickjacking is cheap to exploit and cheap to fix — which makes it an almost unforgivable finding when it shows up in a report. Two response headers, applied consistently, eliminate the entire class of problem.