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
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" }],
},
];
},
};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("; ");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'.
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 freeRelated guides
Content-Security-Policy (CSP): what it is and how to add one
The header that stops injected scripts from running.
MIME sniffing and X-Content-Type-Options: nosniff explained
One header stops browsers from guessing content types and running uploads as scripts.
CORS misconfiguration: the wildcard and reflection traps
Allow every origin and any site can read your API.