Set an X-Frame-Options header
rule · x-frame-options
Clickjacking (opens in a new tab), also called UI redressing, is an attack where a malicious page embeds your application in a hidden or transparent iframe and tricks users into clicking elements they cannot see. X-Frame-Options (opens in a new tab) prevents your page from being embedded as an iframe.
Code Example
┌─────────────────────────────────────┐
│ attacker.com (visible page) │
│ │
│ "Click here to win a prize!" │
│ │
│ ┌─────────────────────────────┐ │ ← Invisible iframe
│ │ bank.com (opacity: 0.0) │ │ positioned over the button
│ │ [Transfer $1000] button │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘The user clicks "win a prize" but actually clicks the hidden "Transfer $1000" button on the bank's page.
Why It Matters
Without framing protection, an attacker can embed your banking login page in a transparent iframe on a malicious site and trick users into clicking buttons they cannot see — transferring money, changing settings, or leaking credentials.
X-Frame-Options Values
| Value | Behavior |
|---|---|
DENY | Page cannot be embedded in any frame, regardless of origin |
SAMEORIGIN | Page can only be embedded by a frame on the same origin |
ALLOW-FROM uri | Obsolete — unsupported in Chrome, Firefox; use CSP frame-ancestors |
Recommended Configuration
Most sites — use DENY
X-Frame-Options: DENYUse this when your pages should never appear inside an iframe (logins, dashboards, payment pages).
Sites with same-origin embeds
X-Frame-Options: SAMEORIGINUse this if your own domain embeds the page (e.g., an admin panel loading pages in an internal iframe).
Server Configuration
Nginx
add_header X-Frame-Options "DENY" always;Apache
<IfModule mod_headers.c>
Header always set X-Frame-Options "DENY"
</IfModule>Next.js
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
},
]
},
}Express.js (using Helmet)
import helmet from 'helmet'
app.use(
helmet.frameguard({
action: 'deny', // or 'sameorigin'
})
)Modern Alternative: CSP frame-ancestors
The Content-Security-Policy: frame-ancestors (opens in a new tab) directive supersedes X-Frame-Options and is more flexible:
# Equivalent to DENY
Content-Security-Policy: frame-ancestors 'none'
# Equivalent to SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
# Allow specific trusted domains (not possible with X-Frame-Options)
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.comframe-ancestors takes precedence over X-Frame-Options in browsers that support both. For maximum compatibility, set both:
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;Exceptions
- A missing or weak header should be evaluated against the live production response path, not only the framework or server config in isolation.
- Legacy integrations or embedded third-party content may require narrowly scoped exceptions, but they should be documented explicitly instead of left permissive by default.
- When multiple security headers are missing, prioritize the header that removes the highest exploitability or browser capability first.
Standards
- Align the implementation with MDN: X-Frame-Options and verify the effective response or browser behavior, not only the configuration file.
- Align the implementation with OWASP: Clickjacking Defense Cheat Sheet and verify the effective response or browser behavior, not only the configuration file.
- Align the implementation with MDN: CSP frame-ancestors and verify the effective response or browser behavior, not only the configuration file.
Verification
Automated Checks
- Inspect the effective response headers with curl, a security header scanner, or equivalent tooling against representative live responses.
Manual Checks
- Payment widgets (Stripe, PayPal) embedded in merchants' sites
- Analytics dashboards embedded in CMS admin panels
- Third-party video players