Skip to content

X-Frame-Options Overridden by Permissive CSP

Severity: Medium

Last updated August 19, 2026.

What this finding means

Your site does say it must not be embedded inside another website's page — the X-Frame-Options header is set. But a second, newer setting on the same site tells browsers that anyone may embed it: your Content-Security-Policy declares a frame-ancestors directive whose whole source list grants everyone, such as * or a bare scheme like https:.

Browsers follow the newer one. Every current browser engine ignores X-Frame-Options outright whenever an enforced CSP carries frame-ancestors, so the permissive policy is what takes effect and the page can be framed. The protection you have set is being cancelled by it.

This is reported separately from a missing header, and deliberately so: your site is not missing X-Frame-Options, and the fix is a different one.

Why it matters

Despite the setting being in place, someone can still load your site inside their own page, invisibly, and trick a visitor into clicking a real button on it while they believe they are clicking something else — a clickjacking attack. That matters most on any page with a state-changing action a single click completes: login, delete, confirm, pay.

The distinctive risk here is not that protection is absent but that it looks present. Reviewing your own configuration, you would see X-Frame-Options set and conclude the control is working. It is not.

What Nivaronix checks

Nivaronix's security headers checker reads both headers together, because a browser does. This finding is reported only when X-Frame-Options is present and an enforced Content-Security-Policy declares a frame-ancestorsdirective whose entire source list is permissive. This is a security misconfiguration check based on the headers' presence and configuration — not a vulnerability scan, exploit attempt, or penetration test.

A frame-ancestors directive that names any real origin is treated as a deliberate, restrictive choice and does not produce this finding — including a host wildcard such as *.example.com, which names a real origin set. Nor does 'self', 'none', or a bare frame-ancestors; with no sources, which browsers treat as 'none'. Where a header carries several policies, browsers enforce all of them, so one restrictive policy is enough. Only the frame-ancestors directive is read; the rest of your policy is not graded here.

The related case — X-Frame-Options absent, with nothing restrictive in its place — is reported as Missing X-Frame-Options instead.

Evidence example

Example only, for illustration — not evidence from a live scan of any specific site.

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors *

# the browser applies frame-ancestors * and ignores X-Frame-Options

How to fix it

Correct the frame-ancestors directive, rather than re-sending the X-Frame-Options header your site already sets and browsers are already ignoring. Either tighten the directive to match what you intended, or remove it so X-Frame-Options takes effect again. Tightening it is the better of the two: frame-ancestors is the modern replacement and supports an allow-list, where X-Frame-Options is all-or-nothing.

Nginx — tighten the directive

# replace `frame-ancestors *` with what you actually allow:
add_header Content-Security-Policy "frame-ancestors 'none'" always;

# or, if named partners legitimately embed this page:
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always;

Apache — tighten the directive

Header always set Content-Security-Policy "frame-ancestors 'none'"

Cloudflare

Rules → Transform Rules → Modify Response Header → edit the existing `Content-Security-Policy` value so `frame-ancestors` names `'none'`, `'self'`, or your specific embedding origins instead of `*`.

Or remove it, if X-Frame-Options is the rule you want

# drop `frame-ancestors` from the policy entirely; with no such
# directive, browsers fall back to the X-Frame-Options you already send.
# Keep every other directive in the policy intact.

If a directive is set to * because something genuinely needs to embed the page, name that origin rather than leaving the wildcard. If nothing does, 'none' is the correct value.

Verify the fix

Re-run a scan on your domain, or check manually with curl -sI https://your-domain.example, and confirm the frame-ancestors directive no longer grants everyone. As a direct test, load the page in an <iframe> on a different origin: a browser honouring the corrected policy will refuse to render it and log a console error.

Standards reference

MDN — CSP: frame-ancestors, which records that frame-ancestors supersedes X-Frame-Options where both are present. See the methodology page for exactly how this finding is scored.