AppSec · Aug 19, 2026 · 6 min read

Clickjacking: confirming frame embeddability is not the same as UI redress

Missing X-Frame-Options or a permissive frame-ancestors directive is reachable - confirming a sensitive interaction is capturable at a predictable position is the step that decides severity.

Clickjacking hides a sensitive action beneath a transparent iframe. A user believes they are clicking a decoy button; the browser delivers the click to the framed target instead. Missing X-Frame-Options or a permissive frame-ancestors directive is reachability - confirming that a specific sensitive interaction is actually capturable is the step that decides whether this is a cleanup item or an incident.

01Two facts that together set severity

Every confirmed clickjacking finding rests on two independent facts. The first is embeddability: the target origin will load inside a cross-origin frame. The second is a capturable sensitive action: a button, a link, or a form submit on the embedded page sits at a predictable, stable position within a fixed viewport, so a decoy page can position a transparent overlay over it precisely. Neither fact alone is the finding. A page embeddable only from the same origin blocks an external attacker. A page that can be framed but exposes only read-only content carries no escalation path worth a critical rating.

Attacker page (http://evil.example) +------------------------------------------+ | decoy button: "Claim your prize" | | transparent iframe, z-index above decoy | | src=https://app.example/account/delete | | [ DELETE MY ACCOUNT ] <-- button at (220,180) | +------------------------------------------+ User clicks decoy ──► browser delivers click to iframe target at (220,180) Account deletion executes. No malformed request. No injected payload.
The attack uses valid browser click routing - no script injection, no CSRF token bypass. The iframe delivers the click as if the user chose it deliberately.

02Confirming embeddability from response headers

Embeddability is a header classification problem. The probe sends a request to the target page and classifies the framing policy from the response. Two headers govern the browser's decision; when both are present, modern browsers give CSP frame-ancestors precedence over X-Frame-Options. Checking only one header when both are set produces a false result in either direction.

clickjack-probe.httphttp
# Step 1 - request the target page as a browser would
GET /account/delete HTTP/1.1
Host: app.example
Sec-Fetch-Mode: navigate

# Step 2 - classify the framing policy from response headers
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
# REACHABLE - no framing protection present
# X-Frame-Options header: absent
# Content-Security-Policy header: absent

# contrast: protected response
Content-Security-Policy: frame-ancestors 'none'  # blocks all framing
X-Frame-Options: DENY                                 # legacy reinforcement
# both present - frame-ancestors wins in modern browsers

A frame-ancestors value of 'self' blocks a cross-origin attack but remains relevant if a stored XSS exists on the same origin. A wildcard frame-ancestors * is as permissive as no header at all. The probe records the exact header values as the confirming artifact rather than a binary pass/fail, because an auditor reviewing severity needs to see the literal policy string, not a summary.

03Severity follows the capturable action

Once embeddability is confirmed, the scanner identifies the sensitive interactions available on the framed page at a stable viewport. Element position is read from the page's layout at a fixed resolution - the confirming artifact is the bounding box of the sensitive element, not a screenshot that triggers the action. No click is simulated against a production session and no irreversible action is taken during the probe.

Proven - CriticalClickjacking - /account/delete - account deletion button
GET /account/delete ──► 200 OK X-Frame-Options: absent Content-Security-Policy: absent Button "Confirm account deletion" at viewport-stable position (left: 218px, top: 176px) Origin cross-check: no SameSite cookie restriction blocks the framed request
Oracle: header classification + element-position fingerprint. No click delivered, no account modified. Severity critical - a one-click overlay over an irreversible destructive action with no CSRF token or re-authentication gate on the framed endpoint.

The capturable action determines severity. A page that only renders read-only profile data is low regardless of embeddability. A page with a payment-confirmation button or an account-deletion trigger at a predictable position is critical. The table below shows how the rating scales with the action the overlay can force:

Capturable actionSeverityReason
Read-only content displayInfo / LowNo state change achievable via a forced click
Follow / like / share actionLowLow-impact social manipulation; no account takeover path
Email or password change formHighAccount takeover via credential substitution
MFA disable or trusted-device addHighWeakens subsequent account protection
Fund transfer or purchase confirmationCriticalDirect financial loss achievable via single overlay click
Account deletion (irreversible)CriticalIrreversible destructive action, one interaction

04Fixing framing policy: CSP frame-ancestors, not X-Frame-Options

The fix is a server-side header that restricts which origins may embed the page in a frame. CSP frame-ancestors is the current standard; X-Frame-Options remains useful as a defense-in-depth fallback for older browsers that do not support CSP. Sending only X-Frame-Options leaves a gap in Safari versions and some embedded WebViews that process the CSP directive but have inconsistent X-Frame-Options handling. Sending only CSP leaves older IE and some legacy proxies unprotected. Both headers, consistently, is the safe baseline.

nginx.confnginx
# Recommended: CSP frame-ancestors with X-Frame-Options fallback
add_header Content-Security-Policy
    "frame-ancestors 'none';"  always;
add_header X-Frame-Options "DENY"  always;

# If same-origin embedding is needed (e.g. a dashboard widget):
add_header Content-Security-Policy
    "frame-ancestors 'self';"  always;
add_header X-Frame-Options "SAMEORIGIN"  always;

# Never: wildcard frame-ancestors or missing both headers
# frame-ancestors * == no protection for cross-origin clickjacking

Re-authentication gates and CSRF tokens on destructive endpoints reduce impact but do not close the framing vector. A user who is already authenticated and whose browser auto-submits session cookies to a framed origin can be walked through a multi-step flow by a sequence of overlays - each step is a separate click, and re-auth prompts disappear if the session is live. The header is the only reliable control.

2
headers checked per endpoint
0
user clicks simulated during probe
6
severity tiers by capturable action

Clickjacking findings fail quietly in both directions without a consistent two-fact confirmation. Reporting embeddability without a capturable action over-claims; missing a permissive wildcard directive because only one header was checked under-reports. The confirming artifact - the exact header values plus the element bounding box on the sensitive endpoint - is what turns a header observation into a rated, evidence-backed finding.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →