Clickjacking is a UI deception attack where an attacker loads your page invisibly inside an iframe and tricks users into clicking buttons they can't see — transferring funds, changing account settings, or authorizing actions without knowing it. X-Frame-Options is one of the oldest and most widely supported defenses against this attack.
It's also one of the most common headers to disappear silently after a deployment. This guide covers how to check whether you have it, what each value means, and how to catch regressions before they matter.
The X-Frame-Options HTTP response header controls whether a browser is allowed to embed your page in a <frame>, <iframe>, <embed>, or <object>. When the header is set, any attempt to load your page inside a frame from an unauthorized origin causes the browser to refuse to render it.
There are three valid values:
| Value | Effect | Recommendation |
|---|---|---|
DENY |
Page cannot be framed by anyone — not even pages on the same origin | Best for most sites |
SAMEORIGIN |
Page can only be framed by pages on the same origin (same protocol + host + port) | OK if you use iframes internally |
ALLOW-FROM origin |
Page can be framed only by the specified origin | Deprecated — use CSP instead |
For most web applications, X-Frame-Options: DENY is the right choice. If your app legitimately embeds its own pages in iframes (e.g. preview panels, widget builders), use SAMEORIGIN.
X-Frame-Options header makes clickjacking technically feasible. Modern browsers also warn about framed pages without this header in developer tools.
The fastest way is to use the free HTTP Security Headers Checker. Enter your domain and click Check — you'll see all security headers in one view, including whether X-Frame-Options is present, what value it has, and whether it's configured correctly.
The checker also shows:
Content-Security-Policy (and whether frame-ancestors is set)Strict-Transport-Security (HSTS)X-Content-Type-OptionsReferrer-PolicyPermissions-PolicyCheck response headers directly:
curl -sI https://yoursite.com | grep -i "x-frame\|x-content\|strict-transport"
Expected output when properly configured:
x-frame-options: DENY
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains
If X-Frame-Options is absent from the output, it's not set. Note that some frameworks set this header only on HTML responses (not on API endpoints or static assets) — check the page that handles authenticated actions specifically.
To check a specific path:
curl -sI https://yoursite.com/dashboard | grep -i "x-frame"
F12 or Cmd+Option+I)x-frame-optionsChrome also flags pages without framing protection in the Security panel — go to Security → click the page origin → check the headers section.
The Content-Security-Policy header with a frame-ancestors directive is the modern replacement for X-Frame-Options. It offers more flexibility (multiple allowed origins) and is supported in all modern browsers.
| Feature | X-Frame-Options | CSP frame-ancestors |
|---|---|---|
| Browser support | Excellent (IE 8+) | Very good (IE not supported) |
| Multiple allowed origins | No | Yes |
| Syntax | X-Frame-Options: DENY |
Content-Security-Policy: frame-ancestors 'none' |
| Overrides X-Frame-Options? | — | Yes, when both are present |
| Recommendation | Keep for legacy browsers | Primary protection in modern stacks |
Best practice: Set both. Use X-Frame-Options: DENY for older browser coverage and Content-Security-Policy: frame-ancestors 'none' for modern browsers. When CSP frame-ancestors is present, it takes precedence in supporting browsers, so there's no conflict.
server {
# ...
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
}
Header always set X-Frame-Options "DENY"
Header always set Content-Security-Policy "frame-ancestors 'none'"
const helmet = require('helmet');
app.use(helmet({
frameguard: { action: 'deny' }, // sets X-Frame-Options: DENY
contentSecurityPolicy: {
directives: {
frameAncestors: ["'none'"], // CSP frame-ancestors: 'none'
},
},
}));
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'none'",
},
],
},
];
},
};
module.exports = nextConfig;
A common pattern: security headers are set in the CDN or reverse proxy config, but API responses or dynamically routed pages bypass the proxy. Check your most sensitive pages explicitly — the login page, checkout, and any page with form submissions — not just the homepage.
Framework version upgrades, CDN config changes, or proxy rule changes can remove security headers without warning. Helmet configuration in Express, for example, changes defaults across major versions. Without monitoring, this goes undetected until a security audit.
The ALLOW-FROM value was never supported in Chrome or Firefox and has been removed from the spec. If you need to allow specific origins to embed your page, use Content-Security-Policy: frame-ancestors https://trusted-origin.com instead.
Cloudflare, AWS CloudFront, and other CDNs can be configured to strip or override response headers. Check both your origin server response and what the CDN delivers to the browser — they can differ.
Setting a header once is not enough. Security headers disappear more often than teams realize — during Nginx config refactors, Helmet upgrades, CDN rule changes, or new service deployments. A regression that removes X-Frame-Options can go unnoticed for months.
The right approach is to monitor continuously and get an alert when any header changes or disappears. HeaderGuard checks your security headers automatically and sends an email alert when any header changes — including X-Frame-Options, Content-Security-Policy, Strict-Transport-Security, and others.
You can also build a basic header regression check into your CI pipeline:
#!/bin/bash
# ci-header-check.sh — fail CI if X-Frame-Options is missing
URL="https://yoursite.com"
EXPECTED="deny" # lowercase — header values are case-insensitive
actual=$(curl -sI "$URL" | grep -i "x-frame-options" | awk -F': ' '{print $2}' | tr -d '\r\n' | tr '[:upper:]' '[:lower:]')
if [[ "$actual" != "$EXPECTED" && "$actual" != "sameorigin" ]]; then
echo "FAIL: X-Frame-Options is '$actual' (expected '$EXPECTED' or 'sameorigin')"
exit 1
else
echo "OK: X-Frame-Options is '$actual'"
fi
Add this to your CI pipeline to catch header regressions before they reach production. For continuous monitoring outside CI, use an automated service so you're alerted even when no deployment is happening — a CDN change can remove headers at any time without triggering a build.
See your X-Frame-Options, CSP, HSTS, and all security headers in one free check. Then set up monitoring to get alerted when they change.
Check Security Headers Free → Monitor for Regressions →