Skip to main content
All fixes

SSL, TLS, security headers

Block clickjacking with X-Frame-Options or CSP frame-ancestors

Without X-Frame-Options or CSP frame-ancestors, attackers can iframe your site and overlay UI to trick clicks. Set frame-ancestors 'none' or 'self'.

What's happening

X-Frame-Options is an HTTP response header that tells browsers whether a page may be loaded in an , , or . Without it (and without a frame-ancestors directive in CSP), attackers can iframe your page on a malicious origin, overlay invisible UI, and trick users into clicking buttons that perform actions on the legitimate site — the classic clickjacking attack.

X-Frame-Options has three values: DENY (no framing at all), SAMEORIGIN (only your own origin), and ALLOW-FROM uri (deprecated, ignored by Chrome). The modern replacement is the CSP frame-ancestors directive, which supports multiple origins and 'none'/'self' keywords. CSP frame-ancestors overrides X-Frame-Options when both are present.

The fix is to set both headers — X-Frame-Options DENY for legacy browsers, CSP frame-ancestors 'none' for modern ones — unless you have a deliberate reason to allow framing (e.g. an embeddable widget).

Why it matters

Authenticated users can be tricked into clicking 'transfer money', 'delete account', or 'change password' on a hidden iframe loaded from an attacker-controlled page. The attack does not require any XSS bug; it exploits the absence of frame protection.

OWASP Top 10 lists clickjacking under 'Security Misconfiguration' and PCI-DSS 4.0 expects frame protection on every authenticated page. SOC2 reports treat the missing header as a control gap.

Mozilla Observatory and SecurityHeaders.com penalize the missing header in their grades, and the missing protection is one of the most common high-severity findings in bug-bounty programs.

Common causes

  • The web server has never set the header.
  • An older configuration set X-Frame-Options ALLOW-FROM, which Chrome ignores entirely.
  • A reverse proxy strips the header before responses leave the edge.
  • A specific embedded route disabled the header and the disable rule applies too broadly.
  • CSP frame-ancestors was set but with a wildcard or overly permissive list.

Detect this on your site

Run a quick scan with the Full Site Audit. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Full Site Audit

How to fix it

  1. 1

    Check current header state

    Run curl -sI https://example.com | grep -iE 'x-frame-options|content-security-policy'. Inspect for an X-Frame-Options value and a frame-ancestors directive in CSP. If neither is present, the page is frameable by anyone.

  2. 2

    Decide on the framing policy

    Most production pages should set 'none' (no framing at all). Sites that legitimately need same-origin framing (admin tools loading widget previews) should use 'self'. Embeddable widgets need an explicit allowlist of permitted parent origins.

  3. 3

    Set X-Frame-Options for legacy clients

    In nginx add add_header X-Frame-Options "DENY" always;. In Apache add Header always set X-Frame-Options "DENY". Use SAMEORIGIN if you frame your own pages internally.

  4. 4

    Set CSP frame-ancestors for modern browsers

    Either add a fresh CSP or extend an existing one with frame-ancestors 'none' (or 'self', or specific origins). frame-ancestors takes precedence over X-Frame-Options in any browser that supports CSP Level 2, which is every browser since 2015.

  5. 5

    Apply per-route exceptions sparingly

    If you have an embeddable widget, set a different CSP on just that route with frame-ancestors https://customer-a.com https://customer-b.com. Do not blanket-allow framing across the whole site to support a single embed.

  6. 6

    Reload and verify

    Reload the web server and re-run the curl command. Confirm both headers are present. Then test by loading the page in a hostile iframe locally — the browser should refuse with 'X-Frame-Options' or 'Refused to display... in a frame because an ancestor violates CSP'.

Example

# Block all framing, both legacy and modern clients
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;

Belt-and-braces frame protection in nginx

Frequently asked

Belt and braces. Every modern browser respects frame-ancestors and ignores X-Frame-Options when both are set but old corporate browsers and embedded WebViews may still rely on X-Frame-Options. The cost of setting both is two header lines.

ALLOW-FROM was specified in RFC 7034 but never implemented by Chrome or Safari. Only Firefox and IE supported it. The replacement is CSP frame-ancestors which takes a list of origins and is supported everywhere.

Related fixes