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 AuditHow to fix it
- 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 aframe-ancestorsdirective in CSP. If neither is present, the page is frameable by anyone. - 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
Set X-Frame-Options for legacy clients
In nginx add
add_header X-Frame-Options "DENY" always;. In Apache addHeader always set X-Frame-Options "DENY". Use SAMEORIGIN if you frame your own pages internally. - 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
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
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
SSL, TLS, security headers
Deploy a Content-Security-Policy to mitigate XSS attacks
SSL, TLS, security headers
Remove unsafe-inline from CSP and adopt nonce-based scripts
SSL, TLS, security headers
Add X-Content-Type-Options nosniff to block MIME sniffing
SSL, TLS, security headers
Set Referrer-Policy to control outbound Referer leakage