Stolen Buttons: What Clickjacking Still Teaches Us About UI Trust

A user clicks what looks like a "Play" button. What they actually clicked was an invisible "Confirm Payment" button floating on top of it. No malware. No phishing email. Just a carefully positioned iframe and a few lines of CSS.

This is the essence of clickjacking — and the concept of "stolen buttons" is a sharp reminder that the UI layer is not as trustworthy as most developers assume.


What Are "Stolen Buttons"?

The idea is straightforward but unsettling. A malicious page embeds a legitimate third-party site inside a transparent or hidden iframe. It then positions that iframe so that an interactive element — a button, a link, a form submit — sits precisely underneath something the user intends to click on the attacker's own page.

The user's click is stolen. It lands on the legitimate site's element, triggering an action the user never consciously authorised.

What makes this particularly interesting from a systems perspective is that nothing in the browser's normal security model breaks down. The same-origin policy is not violated. No JavaScript crosses the iframe boundary. The browser is simply doing exactly what it was designed to do — faithfully registering a click at a set of coordinates. The vulnerability lives in the gap between visual intent and actual target.


Why This Problem Refuses to Die

Clickjacking was formally described and named back in 2008. Sixteen years later, it remains relevant. Why?

The fix is opt-in, not opt-out

The primary defence — the X-Frame-Options HTTP header and its modern replacement, the Content-Security-Policy: frame-ancestors directive — requires the victim site to explicitly declare that it refuses to be framed. If a developer forgets, or a legacy endpoint predates the policy, that page is potentially exploitable.

# Modern, recommended approach
Content-Security-Policy: frame-ancestors 'self'

# Legacy fallback (still widely supported)
X-Frame-Options: SAMEORIGIN

Many SaaS products ship dozens of routes. It takes exactly one unprotected route — perhaps an internal admin action, an OAuth callback, or a payment confirmation — to create a usable attack surface.

SPAs and dynamic rendering create blind spots

Single-page applications render content dynamically. Routes that didn't exist six months ago now do. A CSP policy hardcoded at the server level or set only on the root path may not propagate to every dynamically loaded view. Teams that audit security at deployment time and then move on are building a widening gap between their security posture and their actual attack surface.

Third-party embeds cut both ways

Your product might be the one doing the framing. Chat widgets, analytics dashboards, payment iframes, map embeds — all of these involve intentional cross-origin framing. Getting the trust model right means carefully distinguishing between frames you control, frames you permit, and frames you should never allow at all.


The Deeper Lesson: UI Is a Security Boundary

Most security conversations inside engineering teams focus on data — SQL injection, XSS, insecure APIs, exposed credentials. The UI layer is treated as a presentation concern, not a security one. Clickjacking is a direct challenge to that assumption.

Consider what a stolen button can do in a modern SaaS application:

  • Authorise OAuth scopes — a framed consent screen, clicked by a user who thought they were dismissing a notification.
  • Confirm a destructive action — delete an account, revoke an API key, transfer funds.
  • Trigger a one-click purchase — particularly dangerous in e-commerce flows where the user is already authenticated.
  • Submit a pre-filled form — the attacker controls the surrounding page and can nudge the user to position their cursor exactly where needed.

None of these require breaking encryption or stealing tokens. They abuse the legitimacy of the authenticated session itself.


Practical Steps for Software Teams

Getting this right is not complicated, but it does require deliberate process.

1. Set frame-ancestors globally, at the infrastructure level. Do not rely on application code to set security headers per-route. Configure your reverse proxy — Nginx, Caddy, a CDN like Cloudflare — to inject Content-Security-Policy: frame-ancestors 'none' (or 'self') on every response by default. Override only where you have a deliberate, reviewed reason to allow framing.

2. Audit every route that performs a state-changing action. Payment confirmations, permission grants, account deletions, and similar endpoints deserve explicit framing policies regardless of whether your default policy covers them. Treat these like you would treat CSRF tokens — mandatory, not optional.

3. Add UI-level defences for sensitive actions. Frame-busting JavaScript has a troubled history (it can be defeated), but layered defences still matter. Require re-authentication or a secondary confirmation for high-impact actions. A stolen click is far less useful if the action it triggers demands a password or a TOTP code.

4. Include framing checks in your security review checklist. Add a line to your PR and deployment checklists: Does this route perform a state-changing action? Is frame-ancestors explicitly set? Automation tools like OWASP ZAP and many commercial scanners will flag missing headers during a scan — run them regularly, not just at launch.

5. Be thoughtful about what you embed. If your product intentionally frames third-party content, establish an allowlist. frame-ancestors protects your pages from being framed; Content-Security-Policy: frame-src controls what you are allowed to frame. Both sides of the trust relationship need attention.


The Invisible Attack Surface

What makes clickjacking intellectually interesting — and practically dangerous — is its invisibility. There are no error logs. No failed authentication attempts. No anomalous API calls. A successful clickjacking attack looks, from every monitoring perspective, like a normal user performing a normal action. Detection after the fact is very hard.

That invisibility is a signal: prevention is the only realistic strategy.

Source: Anatoly Zenkov — Stolen Buttons (https://anatolyzenkov.com/stolen-buttons), via Hacker News.


Why this matters for your project: If you are building or scaling a SaaS product, every authenticated route that changes state is a potential stolen button. Security headers are cheap to set and expensive to forget. Review your infrastructure defaults now, before a creative attacker reviews them for you.