All guides
Medium severitySecurity Headers

Clickjacking and X-Frame-Options: keep your pages out of iframes

Clickjacking tricks users into clicking hidden controls on your page by loading it inside a transparent iframe on an attacker-controlled site. X-Frame-Options — or the modern CSP frame-ancestors directive — tells browsers to refuse to embed your page, closing the attack entirely.

What it is

A clickjacking attack works by framing your site inside an invisible or translucent iframe on an attacker's page. The victim sees decoy content and clicks what looks like an innocent button; the hidden iframe captures the real click on your page — a bank transfer, a settings change, an OAuth grant — without the user realising.

X-Frame-Options is an HTTP response header with two useful values: DENY (refuse framing by anyone) and SAMEORIGIN (only allow framing by pages on your own origin). CSP's frame-ancestors directive does the same job with finer control — you can list specific trusted origins, and it supersedes X-Frame-Options in browsers that support CSP.

Why it matters

Any page with a meaningful action — account settings, payment confirmation, email preferences, or an admin toggle — is a clickjacking target. A logged-in user who lands on a malicious page can be tricked into taking real actions they never intended, with no visible sign that anything is wrong.

X-Frame-Options costs nothing to add and is supported by every modern browser. Omitting it on pages that carry out sensitive actions is a straightforward misconfiguration, not a missing feature.

How to fix it

Add X-Frame-Options globally in Next.js

Set the header in next.config.js so it applies to every route. DENY is the safe default for apps that don't need to be embedded.

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [{ key: "X-Frame-Options", value: "DENY" }],
      },
    ];
  },
};
Add frame-ancestors to your CSP

CSP's frame-ancestors directive supersedes X-Frame-Options in modern browsers and gives you per-origin control. Add it to the Content-Security-Policy header you already set.

// extend your existing CSP string
const csp = [
  // ... your other directives ...
  "frame-ancestors 'none'",
].join("; ");
Use SAMEORIGIN if you embed within your own domain

If you legitimately embed pages in your own site — a preview iframe or an embedded widget on the same origin — use X-Frame-Options: SAMEORIGIN instead of DENY, or frame-ancestors 'self'.

Configure nginx

If you're serving responses through an nginx proxy, add the header in your server block.

add_header X-Frame-Options "DENY" always;

FAQ

X-Frame-Options or CSP frame-ancestors — which should I use?

Use both. X-Frame-Options covers older browsers; frame-ancestors in CSP gives you per-origin granularity and is the modern standard. They coexist safely — browsers that support CSP use frame-ancestors and ignore X-Frame-Options.

Does DENY break embedding my own widgets or analytics dashboards?

Yes — DENY blocks all framing, including your own origin. If you need to embed a specific page in an iframe on your own domain, use SAMEORIGIN or frame-ancestors 'self'. Only loosen it for the pages that genuinely need embedding.

What severity does AppSafe report for a missing X-Frame-Options?

Medium. It's not an immediate data-theft vector on its own, but any page that carries out account actions is a real clickjacking risk without it.

Is your app affected?

AppSafe checks for this and dozens of other issues in one free scan.

Scan my app free
Clickjacking & X-Frame-Options — How to Protect Your Pages