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.
Secure, HttpOnly, and SameSite cookie flags
Three flags that keep session cookies from being stolen.
CORS misconfiguration: the wildcard and reflection traps
Allow every origin and any site can read your API.