What this means
The finding means your pages do not tell browsers who may embed them. In a clickjacking attack, a hostile page places your site in a transparent iframe over a decoy, so the victim's click lands on a real button in your app, such as delete, transfer or grant access. The modern control is the CSP frame-ancestors directive. X-Frame-Options is the older header and is still worth sending for older browsers.
Why it happens
- Frameworks and most hosts do not send either header by default.
- Generated apps rarely include security headers unless the prompt asks for them.
- The header was set in a meta tag. Browsers ignore X-Frame-Options and frame-ancestors in meta tags. They must be real response headers.
- Someone used X-Frame-Options: ALLOW-FROM, which modern browsers do not support.
How to fix it
- 1Decide whether any site needs to frame your pages. For most apps the answer is no.
- 2Add both headers to all responses in your framework or hosting config.
- 3If a partner must embed you, list their origin in frame-ancestors, for example frame-ancestors 'self' https://partner.example, and leave out X-Frame-Options on those routes only, since it cannot express an allow-list.
- 4If you already send a Content-Security-Policy header, add frame-ancestors to that policy instead of sending a second CSP header.
- 5Deploy and check the pages that matter most: login, settings, payments and admin.
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
];
},
};
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
/*
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYHow to confirm the fix
Check the headers with curl, then try to frame your own site. Save the HTML below as a local file, replace the URL with your own, and open it in a browser. After the fix the frame stays empty and the console shows a refusal such as 'Refused to display ... in a frame'.
curl -s -D - -o /dev/null https://www.yourapp.com/login | grep -iE 'x-frame-options|content-security-policy'
<!doctype html>
<title>Framing test</title>
<iframe src="https://www.yourapp.com/login" width="800" height="600"></iframe>Frequently asked questions
Do I need both headers?
frame-ancestors is the current standard and takes priority in browsers that support it. X-Frame-Options covers older browsers. Sending both is cheap.
Will this break my embedded widgets, such as a payment form?
No. These headers control who may frame your pages. They do not affect frames your pages load from others. That is controlled by frame-src.
Is SameSite on my cookies enough?
It helps, because a framed cross-site page may load without the session cookie, but OWASP still recommends the framing headers as the primary defence.