All articles
ClickjackingWeb SecurityCSP

Clickjacking Attacks Explained — And How to Block Them

Clickjacking attack explained and how to prevent it: real examples, frame-busting headers, CSP directives, and modern defenses for developers and agencies.

WebSentry TeamJune 22, 20265 min read

Clickjacking is one of those vulnerabilities that sounds almost too simple to be dangerous — until you see a working proof of concept that drains a user's account with a single misplaced click. It's been around since 2008, yet a surprising number of production sites still ship without the headers needed to stop it.

This post breaks down what clickjacking actually is, walks through a realistic attack, and shows you exactly which headers, CSP directives, and cookie settings to configure to shut it down.

What is clickjacking, really?

Clickjacking (also called UI redress) is an attack where a malicious site loads your site inside a transparent <iframe> and tricks a logged-in user into clicking something they didn't intend to. The user sees the attacker's page — a game, a CAPTCHA, a video play button — but their clicks are actually landing on hidden buttons inside your app.

Because the click comes from the real user, in their real browser session, with their real cookies, the action looks completely legitimate to your backend.

A concrete example

Imagine your app has a "Delete account" button at https://yourapp.com/settings. An attacker builds a page like this:

<style>
  iframe { opacity: 0.0001; position: absolute; top: -50px; left: -200px; width: 1000px; height: 1000px; }
  .bait { position: absolute; top: 300px; left: 400px; }
</style>
<button class="bait">Claim your free prize 🎁</button>
<iframe src="https://yourapp.com/settings"></iframe>

The iframe is positioned so the real "Delete account" button sits directly under the fake "Claim your prize" button. The user clicks the prize, the browser sends the click to your app with their session cookie attached, and the account is gone.

The variants you should know about

  • Classic clickjacking — transparent iframe over bait UI, as above.
  • Likejacking — tricks users into clicking social media "like" or "share" buttons.
  • Cursorjacking — uses CSS to display a fake cursor offset from the real one, so users misjudge where they're clicking.
  • Cookiejacking — combines drag-and-drop with iframes to exfiltrate cookie data.
  • Filejacking — abuses file input dialogs to steal local files.

The three defenses that actually matter

You don't need exotic tooling to prevent clickjacking. You need three things configured correctly: a frame-ancestors CSP directive, an X-Frame-Options header as a fallback, and proper SameSite cookies.

1. Content-Security-Policy: frame-ancestors

This is the modern, authoritative defense. The frame-ancestors directive tells browsers which origins are allowed to embed your page in a frame.

To block all framing:

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

To allow only your own domain (for legitimate internal embedding):

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

To allow a specific partner domain:

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

Important: frame-ancestors overrides X-Frame-Options in browsers that support CSP Level 2 (which is essentially all of them now). But set both — older browsers and some embedded webviews still rely on the legacy header.

2. X-Frame-Options as a fallback

The legacy header has three possible values, but only two are useful:

  • X-Frame-Options: DENY — no framing at all, even by your own domain.
  • X-Frame-Options: SAMEORIGIN — framing allowed only from the same origin.
  • ALLOW-FROM uri — deprecated and inconsistently supported. Don't use it; use CSP instead.

For most apps:

X-Frame-Options: DENY

3. SameSite cookies

Even if an attacker frames your page, a clickjack only works if the browser sends authenticated cookies. SameSite=Lax or SameSite=Strict dramatically reduces the attack surface by blocking cookies on cross-site requests.

Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/

Modern browsers default to Lax when no SameSite is set, but you should never rely on browser defaults — set it explicitly.

Setting the headers in real frameworks

Nginx

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

Apache

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

Express (Node.js)

const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.contentSecurityPolicy({
  directives: { frameAncestors: ["'none'"] }
}));

Cloudflare / CDN edge rules

If your origin is hard to modify, add the headers as a Transform Rule or Worker at the edge. Just make sure you're not double-setting conflicting values from both origin and edge.

Common mistakes that leave you exposed

  1. Setting frame-ancestors inside a meta tag. It doesn't work — frame-ancestors only takes effect when delivered as an HTTP header.
  2. Whitelisting too broadly. frame-ancestors * or including wildcard subdomains of platforms you don't control is the same as having no protection.
  3. Only protecting the login page. Every authenticated page — settings, billing, admin actions — needs the header. Apply it globally.
  4. Relying on JavaScript frame-busting. Old tricks like if (top !== self) top.location = self.location can be defeated with sandbox attributes and X-Frame-Bust tricks. Use headers.
  5. Forgetting about new subdomains. Marketing spins up promo.yourdomain.com with default headers off, and suddenly it's a clickjacking liability.

Verifying your defenses

After deploying, don't assume — verify. Quick checks:

  • Open DevTools → Network → click the document request → check the Response Headers panel for both Content-Security-Policy and X-Frame-Options.
  • Build a quick test HTML file with <iframe src="https://yoursite.com"></iframe> and load it locally. The frame should be blank, and the console should log a refusal message.
  • Run a WebSentry scan against your domain. WebSentry checks frame-ancestors, X-Frame-Options, CSP completeness, cookie flags, and dozens of other header-level misconfigurations, then grades the result A–F so you can see at a glance where the gaps are.

When you genuinely need to be framed

Some apps are legitimately embedded — payment widgets, embeddable dashboards, OAuth flows. In those cases:

  • Maintain an explicit allowlist of partner origins in frame-ancestors.
  • Treat the embed origin as untrusted: use postMessage with strict origin checks for any cross-frame communication.
  • Add a visible UI indicator inside your iframe (e.g. the logged-in user's name) so users can spot when something looks off.
  • Re-prompt for authentication on destructive actions, even within a session.

If you want a fast sanity check across all your projects, run them through WebSentry at websentry.dev — it's free, takes about 30 seconds per site, and will flag missing frame-ancestors and weak CSPs before an attacker does.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.