- Home
- HTTP Headers
- X-Frame-Options Header
Header
X-Frame-Options Header
Learn how X-Frame-Options prevents clickjacking attacks by controlling whether your site can be embedded in frames, iframes, or objects on other domains.
TL;DR: Prevents clickjacking by controlling whether your page can be embedded in frames. Use
DENYfor sensitive pages orSAMEORIGINfor general protection.
What is X-Frame-Options?
The X-Frame-Options header prevents clickjacking attacks by controlling whether a browser should allow your page to be displayed in a <frame>, <iframe>, <embed>, or <object>. It’s like putting a “do not frame” instruction on your webpage.
Clickjacking is an attack where a malicious site embeds your page in an invisible iframe and tricks users into clicking on it, potentially performing unintended actions.
Note: X-Frame-Options is being superseded by the frame-ancestors directive in Content-Security-Policy, but it’s still widely used for backward compatibility.
How X-Frame-Options Works
Without protection (vulnerable):
Malicious site:
<html>
<body>
<h1>Win a Free iPhone! Click Here!</h1>
<!-- Invisible iframe over the button -->
<iframe src="https://bank.example.com/transfer" style="opacity: 0; position: absolute; top: 0;">
</iframe>
<button>Click to Win!</button>
</body>
</html>
```text
User clicks "Click to Win!" but actually clicks the invisible bank transfer button.
**With protection:**
```http
HTTP/1.1 200 OK
X-Frame-Options: DENY
Content-Type: text/html
<!DOCTYPE html>
<html>
<h1>Bank Transfer</h1>
<button>Transfer Money</button>
</html>
Browser refuses to load the page in an iframe. Attack prevented!
Syntax
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://example.com
```text
## Directives
### DENY
Prevents the page from being displayed in any frame, regardless of the site.
```http
X-Frame-Options: DENY
Use case: High-security pages like banking, admin panels, authentication.
app.get('/admin/*', (req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY')
next()
})
```text
### SAMEORIGIN
Allows the page to be displayed in a frame only if the frame is from the same origin.
```http
X-Frame-Options: SAMEORIGIN
Use case: Sites that need to embed their own pages but want to block external framing.
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
next()
})
```text
### ALLOW-FROM (Deprecated)
Allows the page to be displayed only in frames from a specific origin.
```http
X-Frame-Options: ALLOW-FROM https://trusted.example.com
Warning: Not supported in modern browsers (Chrome, Safari). Use Content-Security-Policy’s frame-ancestors instead.
Common Examples
Banking Application
HTTP/1.1 200 OK
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Content-Type: text/html
<!DOCTYPE html>
<html>
<h1>Online Banking</h1>
</html>
```text
Prevent any framing for maximum security.
### E-commerce Checkout
```http
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
Content-Type: text/html
<!DOCTYPE html>
<html>
<h1>Checkout</h1>
</html>
Allow your own domain to frame (e.g., for popup checkout) but block others.
Public Content Site
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
Content-Type: text/html
<!DOCTYPE html>
<html>
<article>Public article content...</article>
</html>
```text
Allow embedding on your own domain, block on others.
### OAuth/Authentication Page
```http
HTTP/1.1 200 OK
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Content-Type: text/html
<!DOCTYPE html>
<html>
<h1>Sign In</h1>
<form>...</form>
</html>
Critical: Prevent credential theft via clickjacking.
Real-World Scenarios
Express.js Application
const express = require('express')
const app = express()
// Apply to all routes
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
next()
})
// Override for specific routes
app.get('/embed/video/:id', (req, res, next) => {
// Remove X-Frame-Options to allow embedding
res.removeHeader('X-Frame-Options')
next()
})
app.get('/admin/*', (req, res, next) => {
// Strict protection for admin
res.setHeader('X-Frame-Options', 'DENY')
next()
})
```text
### Path-Based Configuration
```javascript
// Different settings for different sections
app.use((req, res, next) => {
if (req.path.startsWith('/admin') || req.path.startsWith('/auth')) {
// No framing allowed
res.setHeader('X-Frame-Options', 'DENY')
} else if (req.path.startsWith('/embed')) {
// Allow embedding (remove header)
res.removeHeader('X-Frame-Options')
} else {
// Same-origin only
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
}
next()
})
Helmet.js (Express)
const helmet = require('helmet')
// Default: SAMEORIGIN
app.use(helmet.frameguard({ action: 'sameorigin' }))
// Or DENY
app.use(helmet.frameguard({ action: 'deny' }))
```text
### Dynamic Based on User Role
```javascript
app.use((req, res, next) => {
// Admin users get strict protection
if (req.user && req.user.isAdmin) {
res.setHeader('X-Frame-Options', 'DENY')
} else {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
}
next()
})
Clickjacking Attack Examples
Login Form Hijacking
Attacker’s page:
<!DOCTYPE html>
<html>
<body>
<h1>Free Gift Card!</h1>
<p>Enter your email to claim:</p>
<input type="text" placeholder="Email" />
<!-- Invisible iframe overlay -->
<iframe src="https://victim.com/login" style="opacity: 0; position: absolute; top: 0; left: 0;">
</iframe>
<button>Claim Now</button>
</body>
</html>
```text
**Protection:**
```http
HTTP/1.1 200 OK
X-Frame-Options: DENY
<!-- /login page cannot be framed -->
CSRF Token Theft
Attacker’s page:
<!-- Try to steal CSRF token from framed page -->
<iframe src="https://victim.com/form"></iframe>
<script>
// Attempt to read token (blocked by same-origin policy + X-Frame-Options)
const iframe = document.querySelector('iframe')
const token = iframe.contentDocument.querySelector('[name=csrf]').value
</script>
```text
**Protection:**
```http
X-Frame-Options: DENY
Social Media Like Button Hijacking
Attacker’s page:
<!-- Trick user into liking a page -->
<div style="position: relative;">
<button>Click here for prize!</button>
<iframe src="https://social.com/like/page123" style="opacity: 0; position: absolute;"> </iframe>
</div>
```text
**Protection:**
```http
X-Frame-Options: SAMEORIGIN
Migration to Content-Security-Policy
Why Migrate?
X-Frame-Options has limitations:
ALLOW-FROMnot supported in modern browsers- Can’t specify multiple allowed origins
- Being superseded by CSP
frame-ancestors
Modern Approach
# Old
X-Frame-Options: DENY
# New (CSP)
Content-Security-Policy: frame-ancestors 'none'
```text
```http
# Old
X-Frame-Options: SAMEORIGIN
# New (CSP)
Content-Security-Policy: frame-ancestors 'self'
# Old (not supported in Chrome/Safari)
X-Frame-Options: ALLOW-FROM https://trusted.com
# New (CSP)
Content-Security-Policy: frame-ancestors https://trusted.com
```text
### Use Both for Compatibility
```javascript
// Best practice: Set both for maximum compatibility
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY')
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'")
next()
})
Multiple Allowed Origins (CSP Only)
# Not possible with X-Frame-Options
# Only CSP supports this
Content-Security-Policy: frame-ancestors https://trusted1.com https://trusted2.com
```text
## Best Practices
### 1. Set X-Frame-Options on All Pages
```javascript
// ✅ Global protection
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
next()
})
2. Use DENY for Sensitive Pages
// ✅ Maximum protection for critical pages
const sensitivePaths = ['/login', '/admin', '/payment', '/settings']
app.use((req, res, next) => {
if (sensitivePaths.some((path) => req.path.startsWith(path))) {
res.setHeader('X-Frame-Options', 'DENY')
} else {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
}
next()
})
```text
### 3. Combine with CSP
```javascript
// ✅ Defense in depth
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY')
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'")
next()
})
4. Allow Embedding Where Needed
// ✅ Explicitly allow embedding for widgets
app.get('/widget/*', (req, res, next) => {
// Remove X-Frame-Options to allow embedding
res.removeHeader('X-Frame-Options')
// Or use CSP with allowed origins
res.setHeader(
'Content-Security-Policy',
'frame-ancestors https://partner1.com https://partner2.com'
)
next()
})
```text
### 5. Test Framing Behavior
```html
<!-- Test page -->
<!DOCTYPE html>
<html>
<body>
<h1>Test Framing</h1>
<iframe src="https://your-site.com/page"></iframe>
<p>Check console for errors</p>
</body>
</html>
<!-- Expected console error if X-Frame-Options: DENY:
"Refused to display 'https://your-site.com/page' in a frame
because it set 'X-Frame-Options' to 'deny'." -->
Server Configuration Examples
Nginx
# Apply to all locations
add_header X-Frame-Options "SAMEORIGIN" always;
# Or specific location
location /admin {
add_header X-Frame-Options "DENY" always;
}
```javascript
### Apache
```apache
# .htaccess or apache config
Header always set X-Frame-Options "SAMEORIGIN"
# For specific directory
<Directory "/var/www/admin">
Header always set X-Frame-Options "DENY"
</Directory>
IIS (web.config)
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
```text
## Testing
### Check Header
```bash
# Using curl
curl -I https://example.com | grep -i x-frame-options
# Output:
# x-frame-options: DENY
Test Framing
<!-- Create test.html -->
<!DOCTYPE html>
<html>
<body>
<h1>Frame Test</h1>
<iframe src="https://your-site.com"></iframe>
</body>
</html>
<!-- Open in browser and check console for errors -->
```text
### Browser DevTools
```text
1. Open DevTools → Console
2. Try to load framed page
3. Look for error:
"Refused to display '...' in a frame because it set 'X-Frame-Options' to 'deny'."
```javascript
### Automated Testing
```javascript
// Jest test example
test('should set X-Frame-Options header', async () => {
const response = await request(app).get('/admin')
expect(response.headers['x-frame-options']).toBe('DENY')
})
Common Errors and Solutions
Error: Refused to Display in Frame
Refused to display 'https://example.com/page' in a frame because it set 'X-Frame-Options' to 'deny'.
Solution: This is expected behavior. If you need to allow framing:
// Allow same-origin framing
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
// Or allow specific origins with CSP
res.setHeader('Content-Security-Policy', 'frame-ancestors https://trusted.com')
```text
### Conflicting Headers
```javascript
// ❌ Conflicting directives
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'")
// ✅ Consistent directives
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
Browser Support
All modern browsers support X-Frame-Options:
- Chrome: All versions
- Firefox: All versions
- Safari: All versions
- Edge: All versions
- Internet Explorer: 8+
Note: ALLOW-FROM is not supported in Chrome and Safari. Use CSP frame-ancestors instead.
Related Headers
- Content-Security-Policy - Modern alternative with
frame-ancestors - X-Content-Type-Options - MIME sniffing prevention
- Referrer-Policy - Referrer control
- X-XSS-Protection - XSS filter (deprecated)
Frequently Asked Questions
What is X-Frame-Options?
X-Frame-Options prevents your page from being embedded in iframes on other sites. This protects against clickjacking attacks where attackers overlay invisible frames.
What values can X-Frame-Options have?
DENY prevents all framing. SAMEORIGIN allows framing only by same-origin pages. ALLOW-FROM uri is deprecated and not widely supported.
Should I use X-Frame-Options or CSP frame-ancestors?
Use CSP frame-ancestors for new projects as it is more flexible. Include X-Frame-Options for older browser compatibility. They can be used together.
How do I allow specific sites to frame my page?
X-Frame-Options cannot do this reliably. Use CSP frame-ancestors instead: frame-ancestors https://trusted.com allows only that specific origin.