Skip to main content

X-Frame-Options Explained: Preventing Clickjacking in Modern Web Apps

Created: April 24, 2026 CalmOps 3 min read

Introduction

X-Frame-Options is a security response header used to prevent your pages from being embedded in <iframe>, <frame>, or <object> containers on untrusted sites. Its main purpose is to reduce clickjacking risk.

Clickjacking attacks trick users into clicking invisible or disguised UI elements by layering your page under attacker-controlled content.

What Problem Does X-Frame-Options Solve?

Imagine a legitimate banking page loaded inside an attacker page via iframe. The attacker overlays fake buttons and controls where the user clicks. The user believes they are clicking a harmless UI, but the click lands on real sensitive controls in the framed app.

X-Frame-Options tells the browser whether the response is allowed to be framed.

X-Frame-Options Values

The header supports these practical values:

  1. DENY
  2. SAMEORIGIN

Historically, ALLOW-FROM existed in some browsers but is obsolete and not reliable.

DENY

Disallow framing by any origin, including your own.

X-Frame-Options: DENY

Use this for sensitive pages that should never appear in an iframe.

SAMEORIGIN

Allow framing only if parent and framed document share the same origin.

X-Frame-Options: SAMEORIGIN

Use this when you legitimately embed your own pages inside your own app shell.

Browser Behavior Example

If target response includes:

X-Frame-Options: SAMEORIGIN

And an external site tries:

<iframe src="https://your-app.example.com/admin"></iframe>

Most modern browsers will block rendering and show an error similar to:

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

X-Frame-Options vs CSP frame-ancestors

X-Frame-Options is still useful, but Content Security Policy provides a more modern mechanism:

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

Why CSP is better

  1. Supports multiple allowed ancestors.
  2. Better policy expressiveness.
  3. Aligns with broader CSP strategy.

Practical guidance

For modern deployments, set both:

  1. X-Frame-Options: SAMEORIGIN (or DENY)
  2. Content-Security-Policy: frame-ancestors ...

This gives backward compatibility plus modern control.

Server Configuration Examples

Nginx

add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;

For highly sensitive routes, override to DENY.

Apache

Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"

Express.js (Node)

Use Helmet:

import helmet from 'helmet';

app.use(helmet({
 frameguard: { action: 'sameorigin' },
 contentSecurityPolicy: {
  directives: {
   frameAncestors: ["'self'"],
  },
 },
}));

Django

X_FRAME_OPTIONS = "SAMEORIGIN"

SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"

If you use CSP packages, configure frame-ancestors there as well.

Spring Boot

http
 .headers(headers -> headers
  .frameOptions(frame -> frame.sameOrigin())
  .contentSecurityPolicy(csp -> csp
   .policyDirectives("frame-ancestors 'self'"))
 );

Common Deployment Mistakes

  1. Setting header only on some routes but not all HTML responses.
  2. Forgetting always in Nginx so errors and redirects miss headers.
  3. Relying on obsolete ALLOW-FROM.
  4. Assuming API-only JSON endpoints need it (usually unnecessary).
  5. Using iframe-heavy integrations without planning exceptions.

Route-Specific Policy Strategy

In real systems, not all pages share the same framing requirements.

Example strategy:

  1. Login, settings, billing: DENY.
  2. Internal dashboards embedded in same domain shell: SAMEORIGIN.
  3. Partner-embed pages: CSP frame-ancestors with explicit allowlist.

Avoid wildcard or broad partner lists unless strictly required.

Testing Checklist

Quick header check

curl -I https://your-app.example.com

Verify response includes expected security headers.

Browser validation

  1. Create a test page on another origin with iframe pointing to your route.
  2. Open devtools console.
  3. Confirm frame is blocked where expected.

CI automation idea

Add integration tests that assert required headers for security-sensitive routes.

Relationship to Other Headers

X-Frame-Options solves one narrow threat. Pair it with:

  1. Content-Security-Policy
  2. X-Content-Type-Options: nosniff
  3. Referrer-Policy
  4. Strict-Transport-Security
  5. Permissions-Policy

Security headers work best as a bundle, not as isolated toggles.

Conclusion

X-Frame-Options remains a useful baseline defense against clickjacking, but it is no longer the full solution. In 2026, production setups should combine it with CSP frame-ancestors for precise framing control.

If you manage sensitive pages, start with DENY by default and relax only where business requirements demand embedding.

Resources

Comments

Share this article

Scan to read on mobile