Fix: X-Frame-Options Blocking Legitimate Iframes

This guide fixes the case where a trusted partner needs to embed your page in an iframe but X-Frame-Options refuses to display it. The cause is structural: X-Frame-Options has no syntax for allowlisting a specific external origin. The fix is to drop it and express framing rules with the frame-ancestors directive of Content-Security-Policy. The header’s full threat model lives in the cross-origin frame controls reference; here the focus is the exact replacement and how to verify it.

Configuration Syntax & Exact Values

X-Frame-Options accepts only three forms — DENY, SAMEORIGIN, and the dead ALLOW-FROM — none of which can name an arbitrary trusted partner. DENY blocks all framing; SAMEORIGIN allows only your own origin; ALLOW-FROM uri was removed from Chromium and Firefox years ago and is ignored. There is no way to say “allow https://trusted-partner.com and nobody else.” That capability only exists in CSP.

The fix — remove X-Frame-Options and serve:

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com

Unlike X-Frame-Options, frame-ancestors is evaluated by all current browsers and supports multiple origins, wildcards (https://*.partner.com), and scheme/port matching. When both headers are present, browsers honor frame-ancestors — but conflicting signals confuse audits and older clients, so remove X-Frame-Options entirely.

Server-Side Configuration

The change has two parts on every platform: delete the X-Frame-Options line and add (or extend an existing CSP with) frame-ancestors. If you already serve a CSP, append frame-ancestors to that one header rather than emitting a second policy.

Nginx

# Remove this line entirely:
# add_header X-Frame-Options "SAMEORIGIN" always;

add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-partner.com" always;

always forces emission on 4xx/5xx responses; without it the header is dropped from error pages. An add_header in a location block replaces inherited headers, so if any location resets headers, repeat the CSP line there and ensure no stray X-Frame-Options survives.

Apache

# Remove: Header always set X-Frame-Options "SAMEORIGIN"
Header always unset X-Frame-Options
Header always set Content-Security-Policy "frame-ancestors 'self' https://trusted-partner.com"

Header always unset strips any inherited X-Frame-Options from a parent scope so it cannot conflict; always set emits the CSP even on internally generated error documents. Requires mod_headers.

Cloudflare

In Rules → Transform Rules → Modify Response Header: add a rule to Remove header X-Frame-Options, and a second to Set static Content-Security-Policy to frame-ancestors 'self' https://trusted-partner.com. If a Cloudflare managed feature or origin still injects X-Frame-Options, the remove rule strips it at the edge so only the CSP reaches the browser.

Node/Helmet

const helmet = require('helmet');

app.use(helmet({
  frameguard: false, // disables Helmet's X-Frame-Options
  contentSecurityPolicy: {
    useDefaults: false,
    directives: { 'frame-ancestors': ["'self'", 'https://trusted-partner.com'] }
  }
}));

Setting frameguard: false is essential — Helmet emits X-Frame-Options: SAMEORIGIN by default, which would re-introduce the very block you are removing. Register Helmet before route and error handlers.

Diagnostic & Verification Steps

The symptom appears in the embedding partner’s DevTools Console:

Refused to display 'https://yourdomain.com/' in a frame because it set
'X-Frame-Options' to 'sameorigin'.

or, if a stale CSP is involved:

Refused to frame 'https://yourdomain.com/' because an ancestor violates
the Content Security Policy directive: "frame-ancestors 'self'".

Confirm the old header is gone and the new one is present:

curl -sI https://yourdomain.com | grep -iE 'x-frame-options|content-security-policy'

Expected output after the fix:

content-security-policy: frame-ancestors 'self' https://trusted-partner.com

There must be no x-frame-options line. If one still appears, a CDN, app layer, or location block is re-adding it.

Verify the embed loads. Open the partner’s page that hosts the iframe; the document should render. In DevTools → Network → the framed document → Response Headers, confirm the CSP value and the absence of X-Frame-Options. A blocked load still shows the Refused to display/Refused to frame console error pinpointing which header caused it.

Edge Cases, Security Implications & Safe Rollback

  1. Do not leave both headers with conflicting policies. Serving X-Frame-Options: SAMEORIGIN alongside frame-ancestors 'self' https://trusted-partner.com means current browsers allow the partner (they prefer frame-ancestors) while some security scanners and legacy clients still block it on the X-Frame-Options value. The result is intermittent, hard-to-diagnose breakage. Remove X-Frame-Options completely.
  2. Dashboards, embeds, and payment iframes. Each legitimate embedder must be listed explicitly. A status dashboard embedded by https://status.partner.com, an analytics widget, or a hosted payment iframe from a PSP each need their exact origin in the allowlist. Wildcards like https://*.partner.com cover subdomains but never use a bare *, which lets any site frame you and reopens clickjacking.
  3. Per-path policy. Often only one route should be embeddable (an embeddable widget) while the rest of the app stays frame-ancestors 'none'. Scope the permissive policy to that path with a location block (Nginx) or <Location>/.htaccess (Apache), and keep the strict default everywhere else, so you are not exposing the whole origin to framing for one feature.

Rollback directive. This change is non-destructive and not cached as a persistent policy. To revert, restore the previous header value and redeploy; the next response applies immediately with no client-side persistence to clear. The full comparison of when to use each mechanism is covered in X-Frame-Options vs CSP frame-ancestors.

Before and after: XFO block vs frame-ancestors allowlist Before, X-Frame-Options SAMEORIGIN blocks the trusted partner's iframe; after, CSP frame-ancestors allowlists the partner so the embed loads. Before: X-Frame-Options partner.com iframe X your page SAMEORIGIN Refused to display: blocked After: frame-ancestors partner.com iframe your page allowlisted Embed loads The fix Remove: X-Frame-Options: SAMEORIGIN Add: frame-ancestors 'self' https://trusted-partner.com
X-Frame-Options cannot name an external embedder, so it blocks the partner. frame-ancestors allowlists the exact origin and the embed loads.

Frequently Asked Questions

Why can’t X-Frame-Options just allow my partner’s domain? Its grammar has no origin-allowlist form. DENY and SAMEORIGIN are the only working values; the origin-specific ALLOW-FROM was removed from Chromium and Firefox and is ignored. Allowlisting an external embedder is only possible with CSP frame-ancestors.

Should I keep X-Frame-Options for old browsers? No, when it conflicts with your intent. If both are served, modern browsers honor frame-ancestors while legacy clients may still block the partner on X-Frame-Options, producing inconsistent behavior. Remove X-Frame-Options and rely on frame-ancestors, which all current browsers support.

Can I allow more than one embedder? Yes. List each origin space-separated: frame-ancestors 'self' https://a.example https://b.example. Subdomain wildcards (https://*.partner.com) work; a bare * does not — it permits any site and reopens clickjacking.

How do I only allow framing on one page? Scope the permissive frame-ancestors policy to that route with an Nginx location block or Apache <Location>/.htaccess, and keep frame-ancestors 'none' as the default everywhere else.

Conclusion

Roll the change out by first adding frame-ancestors 'self' https://trusted-partner.com in staging, confirming the partner’s embed renders, then removing X-Frame-Options so no conflicting signal remains. Promote to production and verify with curl -sI that only the CSP header is present. Because the policy is not cached client-side, you can scope it per path or revert it with a single immediate deploy if an embedder list changes.