What Is Clickjacking and How to Prevent It on Your Website
Basics3 minInnovialabs editorial team
In short
Clickjacking loads your site in an invisible iframe over a decoy, so users click your button while thinking they click something else. Prevent it by forbidding framing with the CSP frame-ancestors and X-Frame-Options headers.
The short answer
Clickjacking is an attack where someone loads your site in a transparent iframe on their own page and positions it exactly over a harmless-looking button such as "Claim your prize" or "Close ad".
The user sees the decoy but actually clicks the invisible button on your site. If they are logged in to your site at that moment, the action runs on their behalf — with all their permissions and cookies.
What it looks like technically
The attacker's page contains something like this:
<button style="position:absolute; top:200px; left:150px">Claim your prize</button>
<iframe src="https://example.com/settings"
style="opacity:0; position:absolute; top:0; left:0; width:800px; height:600px; z-index:2">
</iframe>
The iframe is fully transparent (opacity:0) and sits above the button (z-index). The coordinates are tuned so that, for example, "Delete account" or "Confirm" lies right under "Claim your prize".
CSRF protection does not help here: the request comes from your own site, inside a real session, with real tokens.
Real-world impact
- Changing settings: making a profile public, turning off security alerts.
- Confirming actions: a transfer, a one-click purchase, a paid subscription.
- Granting permissions: camera or microphone access, connecting a third-party app to the account.
- Likes and follows: inflating social actions in the user's name.
- Deleting data: "Delete" buttons with a single confirmation.
Pages where one or two clicks trigger something important are the most exposed.
How to prevent it
The core rule: the page tells the browser whether it may be framed by other sites. Two response headers do this.
| Header | Value | Effect |
|---|---|---|
Content-Security-Policy | frame-ancestors 'none' | No framing anywhere |
Content-Security-Policy | frame-ancestors 'self' | Framing only on your own origin |
Content-Security-Policy | frame-ancestors 'self' https://partner.example | Your origin plus an explicit list |
X-Frame-Options | DENY or SAMEORIGIN | Legacy header for older browsers |
CSP frame-ancestors is the modern, flexible option. Send X-Frame-Options as well for compatibility. When both are present, modern browsers follow frame-ancestors.
Nginx example:
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Next.js example in next.config.js:
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
],
}];
},
};
Note: frame-ancestors does not work in a <meta> tag, only as an HTTP header.
Extra measures for critical actions:
- Re-authentication: ask for a password or code before account deletion and transfers.
- Session cookies with
SameSite=LaxorStrict— the browser then will not send them inside a frame on another site.
How to test that a page cannot be framed
- Headers: open DevTools, the Network tab, select the document and check for
Content-Security-Policywithframe-ancestorsand forX-Frame-Options. Or runcurl -I https://your-site. - Hands-on test: create a local HTML file with
<iframe src="https://your-site">and open it in a browser. You should see an empty area or an error instead of the site, and the console should report a block due toframe-ancestors. - Check not only the home page but also settings, checkout and admin pages — headers are sometimes set for some routes only.
Common mistakes
- Protecting the home page but leaving the account area without headers.
- Trying to set
frame-ancestorsvia<meta>. - Relying on JavaScript "frame-busting" scripts, which can be bypassed.
- Setting
frame-ancestors *"just in case" to make some widget work.