---
title: Clickjacking Explained: how an invisible frame steals a click
description: Any website can load yours inside an invisible frame and let someone click your buttons while they think they're clicking something else. One header stops it, and almost nothing can go wrong by adding it.
canonical: https://watchfor.io/blog/clickjacking-explained
---

# Clickjacking Explained: how an invisible frame steals a click

2026-09-08 · WatchFor Team · Security · security, web

Any website can load yours inside an invisible frame and let someone click your buttons while they think they're clicking something else. One header stops it, and almost nothing can go wrong by adding it.

Your site can be loaded inside another website. Not linked to — **loaded**, in a frame, on a page you've never seen, by someone you've never met. That's the default, and it has been since frames were invented.

Usually nothing comes of it. Occasionally someone builds a page whose only purpose is to make you click a button in *your* app while you believe you're clicking something in *theirs*. That's clickjacking, and the fix is one line.

## How the trick works

The attacker builds an ordinary-looking page — a prize wheel, a video player, a "click to continue" button. Then:

1. They load your site in an `<iframe>` on top of their page.
2. They make the frame invisible: `opacity: 0`.
3. They line up their bait button with the real button in your interface — "Delete account", "Approve payment", "Grant access", "Transfer".
4. The visitor clicks the thing they can see. The click lands on the thing they can't.

The crucial part: **the visitor is logged in to your site already.** The browser sends their cookies with the frame like it would with any other visit, so as far as your server is concerned, they really did click it. Because they really did.

Nothing is exploited here. No bug, no injection, no stolen password. Two entirely normal features — frames and cookies — are simply pointed at each other.

## Why it isn't obvious you're exposed

There's nothing to see. Your site works. Your logs look normal — the requests come from real, logged-in users doing real, valid actions. The only trace is that the users didn't mean to do them, and that isn't a field you can query.

This is why the check appears in every [website audit](/smart-website-audit) even though everything else is green. Frameability is not a malfunction; it's a setting you haven't chosen yet.

## The fix, in one line

Two headers do the job. Send whichever you can, or both:

```
X-Frame-Options: SAMEORIGIN
```

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

`X-Frame-Options` is the old one, understood everywhere. `frame-ancestors` is part of [CSP](/blog/content-security-policy-explained), is more flexible, and wins where both are present in modern browsers. Sending both is normal and costs nothing.

| Value | Meaning |
| --- | --- |
| `X-Frame-Options: DENY` | Nobody may frame this page, including you |
| `X-Frame-Options: SAMEORIGIN` | Only pages on your own domain may frame it |
| `frame-ancestors 'none'` | The CSP equivalent of `DENY` |
| `frame-ancestors 'self'` | The CSP equivalent of `SAMEORIGIN` |
| `frame-ancestors 'self' https://partner.example` | Your own pages, plus one named partner |

> **`ALLOW-FROM` doesn't work.** You'll find it in old blog posts and old configs. No current browser supports it. If you need to allow a specific partner, that's `frame-ancestors` in a CSP — the only mechanism that actually does it.

## What could break

Genuinely very little, which is why this is the easiest security header to ship. Ask two questions:

1. **Does anything embed our pages on purpose?** Documentation widgets, an embedded checkout, a partner portal, a status page dropped into someone's intranet, an OAuth consent screen. If yes, name those origins in `frame-ancestors` rather than sending `DENY`.
2. **Do we embed ourselves?** An admin panel that loads a preview of a public page, for example. `SAMEORIGIN` covers that; `DENY` does not.

If neither applies — the case for most sites — `SAMEORIGIN` is safe and you're done.

## The parts a header can't fix

- **It only protects framing.** A page that isn't framed at all — a phishing copy of your login screen on a lookalike domain — is a different problem.
- **It doesn't help with `target=_blank` links or pop-ups.** Different mechanism, different mitigation.
- **Destructive actions still deserve a confirmation step.** A header stops the invisible frame; a "type DELETE to confirm" step stops the ordinary mis-click too. Defence in depth means both.

## Keeping it there

Like every response header, this one lives in a config file that gets rewritten — a new reverse proxy, a framework upgrade, a CDN rule that strips unknown headers. Nothing breaks when it disappears, and nothing tells you.

The free [HTTP header checker](/http-header-checker) shows what your site sends right now, and the [Smart Website Checker](/smart-website-audit) reviews all six recommended headers in one scan. To catch a regression rather than discover it, an [API monitor](/docs/monitors/api) can assert the header on every check.

## The bottom line

| | In one line |
| --- | --- |
| **What** | A header saying who may load your pages inside a frame. |
| **Why** | Stops an invisible frame turning a visitor's click into an action in your app. |
| **Danger without it** | Logged-in users perform real actions they never intended — and your logs agree with the attacker. |
| **Add** | `X-Frame-Options: SAMEORIGIN` plus `frame-ancestors 'self'`. |
| **Check first** | Whether anything legitimately embeds your pages. |

Of all the hardening on the list, this is the one with the best ratio: two minutes of work, essentially no risk, and it closes an attack that needs no bug in your code at all.

Related: [Content Security Policy explained](/blog/content-security-policy-explained), [Referrer-Policy explained](/blog/referrer-policy-explained), [HSTS explained](/blog/hsts-explained).

---

Canonical page: https://watchfor.io/blog/clickjacking-explained · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
