Originally published on the Bug Circuit blog.
Clickjacking is an attack where a hacker hides your website inside an invisible frame on their own page, then tricks a visitor into clicking something they can't see — a "Buy Now" button, a "Follow" button, or an account-settings toggle that actually belongs to your site.
This is for anyone who owns or runs a website — WordPress, Shopify, or a custom app — and wants to know if clickjacking is something to worry about. In a few minutes you'll understand exactly how the invisible-overlay trick works, see the real damage it can do, and get the one HTTP response header (plus copy-paste code for the platforms people actually use) that shuts it down for good.
What is clickjacking, actually?
Clickjacking, also called a "UI redress attack," is a trick, not a break-in. The attacker doesn't steal a password or hack your server — they abuse a normal browser feature called an iframe, which lets one webpage embed another webpage inside it, like a window within a window.
Here's the setup:
- The attacker builds their own page with something tempting on it — "Click to claim your prize," a fake video play button, a captcha.
- Behind that visible content, they load your real website inside an iframe and set its transparency to zero, making it completely invisible to the visitor.
- They line up your site's real button — say, "Confirm Purchase" or "Delete Account" — exactly under the fake button the visitor sees.
- The visitor clicks what looks like the prize button, but the click actually lands on your invisible page underneath, in the visitor's real logged-in session.
Because the click happens on your real site with the visitor's real cookies, your server has no way to tell a genuine click from a hijacked one — the request looks completely normal. That's why the fix has to happen at the browser-framing level rather than inside your app's logic; see OWASP's Clickjacking Defense Cheat Sheet for the full technical breakdown. The technique was named and publicized by security researchers Jeremiah Grossman and Robert Hansen in 2008, and it's now formally tracked as CWE-1021: Improper Restriction of Rendered UI Layers or Frames.
What can an attacker actually make your visitors do?
Anything a logged-in user can do with a single click is fair game:
- Force a purchase or subscription renewal on a checkout or "buy now" button
- Change account settings — recovery email, password reset options, two-factor auth
- Grant browser permissions, like "Allow" prompts for camera or microphone
- Manipulate social proof — trick visitors into liking, following, or sharing something ("likejacking")
- Trigger a donation or money transfer on sites with one-click payment buttons
- Delete content or data behind a "Confirm delete" button that was never meant to be clicked blind
None of this needs stolen credentials. It just needs your pages to be frameable, and either a logged-in visitor or an action that doesn't require login at all, like a public donation form.
The 1-line fix: a frame-protection header
The fix is a single line in your server's response headers telling browsers "never let another site put my pages inside a frame." Once that header is present, the invisible-overlay trick can't render at all — the browser simply refuses to load your page inside the attacker's frame.
Two headers do this job. Set both if you can; if you only add one, make it the CSP version.
| Header | The 1 line | What it does | Notes |
|---|---|---|---|
| Content-Security-Policy | Content-Security-Policy: frame-ancestors 'self'; |
Only your own site can frame your pages | Modern standard; can list multiple trusted origins |
| X-Frame-Options | X-Frame-Options: SAMEORIGIN |
Same idea, older syntax | Reliable fallback for older clients; use DENY if nothing should ever frame the page |
Both are documented on MDN: X-Frame-Options and CSP frame-ancestors.
Copy-paste it for your platform
Nginx — add inside your server block:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
Apache — add to .htaccess or your vhost config:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self';"
WordPress (no code needed): many security plugins expose a "Clickjacking protection" or "Frame options" toggle in settings. If your host or plugin offers it, that's the fastest route.
Node.js / Express — using the popular helmet middleware:
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'sameorigin' }));
app.use(helmet.contentSecurityPolicy({
directives: { frameAncestors: ["'self'"] }
}));
Cloudflare (no origin access needed): Rules → Transform Rules → Modify Response Header → add both headers with the values above. This works even if you can't edit your hosting config directly.
How to check if you're actually protected
Don't take it on faith — verify the header is live on your production pages, not just sitting in a config file that never shipped:
- Open your site, open browser DevTools → Network tab, click the main document request, and check the Headers.
- Look for
x-frame-optionsorcontent-security-policyin the response. - Or skip the manual digging and run your domain through Bug Circuit's free security headers checker — it flags missing frame protection alongside the other headers that matter, like HSTS and X-Content-Type-Options, in one pass.
Common mistakes that undo the fix
- Setting it in code but not in production. A CSP meta tag in your HTML, or a header only set in your dev environment, doesn't help real visitors hitting your live, cached pages.
-
Using
DENYwhen you legitimately embed your own content, like a payment widget or dashboard loaded from a subdomain.DENYblocks that too — useSAMEORIGIN, or list the specific trusted origin inframe-ancestorsinstead. - A CDN or proxy stripping the header. Some caches serve stale responses from before the header existed. After deploying, purge the cache and re-check.
- Only testing the homepage. Checkout, account settings, and admin login are the actual targets — confirm the header is present sitewide, not just on the one page you happened to check.
Is this the only clickjacking-style trick?
Frame-based clickjacking is the classic version and by far the most common in the wild, but related tricks exist too — cursorjacking (offsetting the visible cursor from the real one), drag-and-drop hijacking, and tapjacking on mobile. The frame-ancestors/X-Frame-Options fix closes off the frame-based attack entirely. A full manual review checks for the less common variants too, and confirms your specific high-value pages — checkout, settings, delete-account — aren't exposed in ways a header alone can't catch.
When a header isn't the whole story
Adding this header takes minutes and shuts down an entire class of attack — but it's one line among dozens of things that can go wrong on a small business site: exposed admin panels, outdated plugins, weak session handling, misconfigured payment flows. If no one has actually looked at your site's security before, our guide on figuring out if your website is hackable is a good next stop, and if you're weighing an automated scanner against a real person checking your site, see manual vs. automated penetration testing for how they actually differ.
Key takeaways
- Clickjacking hides your site in an invisible iframe so visitor clicks land on your buttons instead of the fake ones they see.
- The fix is one HTTP header:
Content-Security-Policy: frame-ancestors 'self';— addX-Frame-Options: SAMEORIGINtoo for broader compatibility. - Use
SAMEORIGIN/'self'instead ofDENYif you ever embed your own pages inside your own site. - Verify the header on your live production pages, not just in config — CDNs can keep serving stale, unprotected responses.
- Run a free check at Bug Circuit's security headers tool to confirm it's actually in place.
Frame protection is a five-minute fix once you know it's missing — the harder part is knowing what else might be. A Circuit audit is $49, done by an actual person, and comes back with every issue like this one spelled out with the exact fix, not just a scanner score.
