Skip to content
pwnsy
web-securityintermediate#clickjacking#web-security#owasp#csp#appsec

Clickjacking Explained: UI Redress Attacks & Frame Defenses

How clickjacking hijacks a user's clicks with transparent iframes, what an attacker can trigger, and how to block framing with headers and CSP.

Most web attacks target the server: a parser that trusts the wrong input, a query built from a string. Clickjacking targets the person. It exploits the fact that a user decides what to click based on what they can see, and the browser has no way to know that what they saw was a lie. By stacking a real, sensitive page underneath a decoy the victim believes they are using, an attacker collects genuine clicks and points them at buttons the victim would never press on purpose.

The technique is old and the mechanics are simple, which is exactly why it keeps working on applications that never got around to setting a single response header.

The core idea

A clickjacking page loads the target site inside an iframe and makes that frame invisible, usually by setting its opacity close to zero. On top, or rather underneath, the attacker builds a lure: a page that says "Click here to claim your prize" or a small game where the user has to click a button in a specific spot. The frame is positioned so that the target's sensitive control (a "Confirm" button, a "Delete account" link, a permission dialog) sits exactly where the lure invites the user to click.

The victim sees the decoy. Their cursor is over the decoy. But because the invisible frame is layered on top and receives the pointer events, the click actually lands on the framed site. The framed site is loaded in the victim's own browser with the victim's own session, so the request that follows is fully authenticated and completely normal from the server's perspective. Nothing was forged. The user really did click.

What an attacker can trigger

Clickjacking is only as powerful as the actions a single click can complete on the target site. The higher the value of one-click actions, the worse it gets:

  • Confirming a funds transfer or a purchase that was pre-filled
  • Changing an account setting such as an email address or recovery option
  • Granting an OAuth authorization or a device permission
  • Enabling a feature, following an account, or posting content
  • Toggling a security control to a weaker state

Any workflow that turns a meaningful state change into one confirmation click is a candidate. Multi-step flows can be attacked too, by chaining several framed clicks across a sequence of decoys.

The request looks legitimate

From the server's point of view, a clickjacked action is indistinguishable from a real one. Same session cookie, same origin behavior, same everything. This is why server-side input validation does nothing to stop it. The defense has to prevent the page from being framed in the first place, which is a decision the response itself must declare.

Variants worth knowing

The core pattern extends beyond single clicks:

  • Strokejacking. Instead of a click, the attacker captures keystrokes and directs them into a hidden framed input, for example steering typed characters into a field on the target site.
  • Drag-and-drop attacks. The user is induced to drag an object across the screen, and the drag path moves data into or out of the hidden frame, which can extract or inject content.
  • Likejacking. A historical, narrow form aimed at social widgets, where the hijacked click is a "like" or follow action. The label stuck as a reminder that even low-value clicks are worth stealing at scale.

The common thread is that the victim's genuine input is redirected to a target they cannot see.

Why framebusting scripts are not enough

Before headers existed, sites tried to defend themselves with JavaScript that checked whether the page was the top-level window and, if not, tried to break out of the frame or blank the content. This "framebusting" approach has a long history of bypasses. Attackers used sandboxed iframes to neutralize the script, raced the navigation, or abused browser quirks to stop the bust from running. Framebusting can still be a small extra layer, but it must never be the only control, because it runs inside the very context the attacker controls the loading of.

Header-based defense

The durable fix moves the decision out of page script and into the HTTP response, where the browser enforces it before rendering. Two mechanisms exist.

ControlWhat it doesNotes
Content-Security-Policy: frame-ancestorsDeclares exactly which origins may frame the pageCurrent standard; supports an allowlist and 'none'
X-Frame-OptionsLegacy header allowing DENY or SAMEORIGINCannot express multiple allowed origins; kept for old clients

The modern control is the CSP frame-ancestors directive. To forbid all framing of a sensitive page, send:

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

To allow only your own origin to frame it:

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

To allow a specific partner in addition to your own site, list the origins explicitly. This allowlist ability is why frame-ancestors supersedes X-Frame-Options, which only understood "deny everyone" or "same origin only" and could not name a third party.

X-Frame-Options still has value for older browsers that predate CSP framing support, so many sites send both. Where they conflict, treat frame-ancestors as the authoritative one and keep X-Frame-Options as a compatibility fallback.

Default to no framing

Unless a page is specifically designed to be embedded (a widget, an intentional partner integration), it should refuse to be framed. Set frame-ancestors 'none' as the baseline for application pages, then relax it only for the specific routes and origins that genuinely need embedding. Deny by default keeps you from forgetting to protect the one sensitive page that mattered.

How to defend against clickjacking

  1. Send frame-ancestors on every sensitive page. Use 'none' for anything that should never be embedded and an explicit origin allowlist for the rare page that should. Make it part of your default response headers so new routes inherit it.
  2. Keep X-Frame-Options as a fallback. Send DENY or SAMEORIGIN alongside CSP for clients that do not honor frame-ancestors.
  3. Require re-authentication or a second step for high-value actions. A click alone should not move money or change security settings. A password prompt, a typed confirmation, or a step that cannot be pre-positioned defeats a single stolen click.
  4. Set cookies with SameSite where appropriate. This does not stop framing on its own, but it limits how framed and cross-site requests carry session context.
  5. Test the headers, do not assume them. Verify with the browser developer tools or a header scanner that the response actually carries the directive on the routes that matter, including error pages and authenticated views.

Clickjacking never touches your server logic, so no amount of backend hardening removes it. It lives entirely in whether the browser will let your page be layered under someone else's. Attackers fold UI redress into wider campaigns alongside phishing and session theft, and our Exploit Intelligence dashboard tracks how these techniques show up in active exploitation. Declare that your page cannot be framed, protect the response headers across every sensitive route, and gate one-click state changes behind a step an attacker cannot pre-align, and the attack loses its foundation.

A worked example, layer by layer

Walking through the construction of an attack makes the mechanics concrete. Suppose a target application has a page at a settings route with a single button labeled "Delete my account", and clicking it performs the deletion in one step for a logged-in user. That one-click destructive action is the ingredient an attacker looks for.

The attacker builds a page of their own and hosts it somewhere the victim might visit, perhaps promoted through a link in a message or an ad. On that page they place an iframe whose source is the target's settings route. They style the frame with a very low opacity, close to zero, so the victim cannot see it, and they set its dimensions and position so the hidden "Delete my account" button lands over a spot on the visible decoy.

The decoy is the persuasion layer. It might be a headline that says "You have won. Click the green button to claim", with a large, inviting button positioned exactly where the hidden delete control sits underneath. The attacker adjusts the frame's coordinates until the alignment is pixel-accurate. Because the frame sits on top in the stacking order and is transparent, the victim sees the decoy button but the pointer events pass to the framed control.

The victim clicks. Their browser is logged into the target with a valid session cookie, so the delete request goes out fully authenticated. The server sees a normal, authorized request from a real session and completes the deletion. Nothing was forged, no credential was stolen, and the server has no way to know the click was tricked. The only place this could have been stopped is in the browser, before it agreed to render the target inside the attacker's frame.

The same construction generalizes. Swap the delete button for a "Confirm transfer" control, an "Authorize" button on a consent screen, or a toggle that weakens a security setting, and the attack transfers directly. The value of the attack scales with the value of whatever a single authenticated click can accomplish on the target.

A more elaborate version chains several framed clicks. The attacker builds a decoy that asks the victim to click in a sequence of spots, framed as a game or a step-by-step reward, and aligns each spot over a control in a multi-step flow on the target. Each click advances the target's workflow while the victim believes they are progressing through the decoy. This is how attackers reach actions that are not single-click, at the cost of a more fragile alignment that has to survive each step of the target's interface.

Detection signals

Clickjacking is prevented at the response layer, but there are indicators worth watching to know whether pages are exposed or whether framing is being attempted.

  • Sensitive routes without framing headers. The clearest signal is a page that performs state changes and returns neither a Content-Security-Policy: frame-ancestors directive nor X-Frame-Options. A header scan across authenticated routes surfaces the gaps.
  • Referer or origin anomalies on sensitive actions. Requests for high-value actions arriving with a Referer or Origin that is an unfamiliar external site can indicate that the page was framed elsewhere, though these headers are not always present and should not be relied on as a sole control.
  • Framing reports from a report-only policy. Deploying a Content Security Policy in report-only mode can surface attempts to embed pages where they should not be, giving visibility before enforcement.
  • User reports of unexpected state changes. Accounts that report settings changing or actions occurring that the user does not remember performing can point to UI redress, especially when the actions are one-click and the user recalls visiting an unrelated page.
  • Unexpected embedding in third-party pages. Monitoring for your sensitive pages being loaded in frames on domains you do not control, where such telemetry is available, is a direct sign of an attempt.

The honest position is that clickjacking generates weak runtime signals, because a successful attack looks like a legitimate action. This is exactly why the emphasis falls on prevention through headers rather than detection after the fact.

Clickjacking is often confused with attacks that share its cross-site character. The distinctions matter, because the defenses differ.

AttackWhat is abusedWhere it operatesPrimary defense
ClickjackingThe visual layer and genuine clicksThe browser rendering layerframe-ancestors, X-Frame-Options
CSRFImplicit sending of session cookiesThe request layerAnti-CSRF tokens, SameSite cookies
XSSInjected script running in the pageThe page's own originOutput encoding, CSP script controls
Open redirectTrust in a redirect targetThe navigation layerAllowlist redirect destinations
TabnabbingA backgrounded tab rewriting itselfCross-window referencesrel="noopener" on external links

The pairing that causes the most confusion is clickjacking and CSRF. Both cause an authenticated action the user did not intend. The difference is the mechanism: CSRF forges a request that the browser sends automatically with its cookies, and it is defeated by requiring an unguessable token or a SameSite cookie policy. Clickjacking makes the user genuinely perform the click, so anti-CSRF tokens do not help, because the token is present in the framed page and travels with the real click. Only preventing the framing, or requiring an extra deliberate step, stops it.

SameSite is not a clickjacking fix

It is tempting to assume that setting cookies to SameSite closes clickjacking, since it limits cross-site request behavior. It does not. In a clickjacking attack the request originates from the framed target's own origin as a top-level authenticated interaction, so a SameSite cookie is often still sent. SameSite is a valuable control against CSRF and reduces cross-site session exposure, but the control that actually stops framing is frame-ancestors.

Common mistakes

Several recurring errors leave applications exposed even when the team believes clickjacking is handled.

Relying on framebusting script alone. JavaScript that tries to break out of a frame runs inside the context the attacker controls the loading of, and it has a long history of bypasses through sandboxed frames and navigation races. It can be a minor extra layer, but it must sit behind header-based controls, never in place of them.

Setting the header on the login page only. Protection has to cover every route that performs a sensitive action, including authenticated pages deep in the application, confirmation dialogs, and even error pages that expose controls. Attackers target the specific one-click action, so a single unprotected sensitive route undoes the effort.

Using X-Frame-Options with an origin value. The ALLOW-FROM form of X-Frame-Options was inconsistently supported and is effectively obsolete. It cannot express multiple origins reliably. Where an allowlist is needed, use CSP frame-ancestors, which was designed for exactly that.

Forgetting to test the deployed response. Headers set in code or configuration can be stripped or overridden by a proxy, a CDN, or a framework default. The only reliable check is inspecting the actual response the browser receives on the routes that matter, not assuming the configuration took effect.

Treating framing as always malicious. Some pages are meant to be embedded, such as documented widgets or partner integrations. The goal is deny by default with a precise allowlist for the routes and origins that genuinely need embedding, so protection does not break legitimate use and legitimate use does not force blanket permissiveness.

How the defenses evolved

Early web applications had no dedicated control against framing, and the first countermeasures were framebusting scripts written in JavaScript. Attackers steadily defeated them, which drove browsers and standards bodies to move the decision into the HTTP response where page script could not interfere.

The X-Frame-Options header emerged as the first response-level control, offering DENY and SAMEORIGIN. It solved the common cases but could not express an allowlist of several trusted origins, and its ALLOW-FROM extension was poorly and inconsistently implemented. Content Security Policy then introduced the frame-ancestors directive, which expresses 'none', 'self', and an explicit list of permitted origins in a standardized way. That directive is now the authoritative control, with X-Frame-Options kept as a fallback for older clients. The trajectory has been consistent: move enforcement out of the page and into the browser, and give defenders precise control over exactly who may embed a page.

Tapjacking, the mobile relative

The same idea appears on touchscreens under the name tapjacking. On mobile, an application or an overlay draws a decoy on top of a sensitive control, and the user's tap passes through to the element beneath. The classic setting is a malicious app that draws a transparent or misleading overlay over a system permission prompt, so a tap the user believes grants something harmless actually grants a dangerous permission.

The mechanics mirror browser clickjacking: a visual layer the user trusts sits over an action the user cannot see, and the genuine tap is redirected. The defenses differ in form but share the principle. Mobile platforms provide ways for a sensitive view to refuse taps when it is obscured by another window, and they increasingly restrict which apps may draw overlays over others. Application developers building sensitive flows should opt into those obscured-touch protections so a tap on a critical control is rejected when something is drawn on top of it. The lesson carries across form factors: when the visual layer can be controlled by an attacker, the safe default is to refuse input that arrives through an obstruction.

Frequently asked questions

Is clickjacking still relevant now that browsers are more capable? Yes. The attack does not depend on a browser flaw, it depends on an application failing to declare that its pages cannot be framed. Any sensitive page that ships without frame-ancestors or X-Frame-Options is exposed regardless of how modern the browser is, and plenty of applications still omit those headers on interior routes.

Does HTTPS protect against clickjacking? No. HTTPS protects the confidentiality and integrity of data in transit, but it says nothing about whether a page may be embedded in a frame. A page served over HTTPS is just as framable as one served over HTTP unless it sends the appropriate framing headers.

Can I stop clickjacking purely with JavaScript? Not reliably. Framebusting script runs in a context the attacker influences and has a long record of bypasses. Use header-based controls as the real defense and treat any script-based measure as a small supplementary layer at most.

What is the single most important control? Sending Content-Security-Policy: frame-ancestors 'none' on every page that should never be embedded, and an explicit origin allowlist on the rare pages that should. Make it a default response header so new routes inherit it automatically rather than depending on someone remembering per page.

Does clickjacking let an attacker steal my data directly? Usually not directly. Its power is causing an authenticated action, so its impact is whatever a single click can accomplish on the target, such as confirming a change or granting a permission. Related variants like drag-and-drop attacks can move data, but the classic form is about triggering an action rather than reading content.

Why not require confirmation on every single action? Requiring a deliberate second step, such as re-entering a password or typing a confirmation phrase, is an excellent defense for high-value actions, and it defeats a single stolen click. Applying it to every action would harm usability, so the practical approach is to gate the genuinely sensitive, irreversible actions and rely on frame-ancestors to protect the rest.

How do I verify my headers are actually set? Load the page and inspect the response headers in the browser developer tools, or use a command-line request or a header-scanning tool. Check the specific authenticated and sensitive routes, going well beyond the home page, and confirm the directive survives any proxy or CDN in front of the application.

Should I still send X-Frame-Options if I have frame-ancestors? Sending both is a reasonable belt-and-suspenders choice. Modern browsers honor frame-ancestors and treat it as authoritative, but some older clients understand only X-Frame-Options. Send DENY or SAMEORIGIN alongside the CSP directive so legacy clients are covered, and keep frame-ancestors as the control that expresses any allowlist you need.

Sources & further reading

Sharetwitterlinkedin

Related guides