iframe Security
Secure iframes with sandbox attribute and frame policies
iframe Security
iframes can be dangerous security vectors. This guide covers how to embed third-party content safely using sandbox, CSP, and other security features.
iframe Risks
<!-- ❌ DANGEROUS: No restrictions -->
<iframe src="https://untrusted.com/widget"></iframe>
<!-- Can:
- Run scripts
- Access parent page
- Navigate top window
- Submit forms
- Access cookies
- Show popups
-->sandbox Attribute
The sandbox attribute restricts what iframe can do.
Empty sandbox (Most Restrictive)
<!-- ❌ Everything blocked -->
<iframe src="https://untrusted.com" sandbox></iframe>
<!-- Blocked:
- JavaScript execution
- Form submission
- Pop-ups
- Top navigation
- Same-origin access
-->Selective Permissions
<!-- ✅ Allow only what's needed -->
<iframe
src="https://untrusted.com"
sandbox="allow-scripts allow-same-origin"
></iframe>sandbox Flags
allow-scripts
<!-- Allow JavaScript execution -->
<iframe sandbox="allow-scripts" src="..."></iframe>Warning: Combining allow-scripts + allow-same-origin can bypass sandbox!
allow-forms
<!-- Allow form submission -->
<iframe sandbox="allow-forms" src="..."></iframe>allow-popups
<!-- Allow window.open() -->
<iframe sandbox="allow-popups" src="..."></iframe>allow-same-origin
<!-- Treat content as same-origin -->
<iframe sandbox="allow-same-origin" src="..."></iframe>Warning: Never use with allow-scripts for untrusted content!
allow-top-navigation
<!-- Allow navigating top window -->
<iframe sandbox="allow-top-navigation" src="..."></iframe>allow-modals
<!-- Allow alert(), confirm(), etc. -->
<iframe sandbox="allow-modals" src="..."></iframe>allow-downloads
<!-- Allow downloads -->
<iframe sandbox="allow-downloads" src="..."></iframe>allow-presentation
<!-- Allow Presentation API -->
<iframe sandbox="allow-presentation" src="..."></iframe>Common Combinations
YouTube Embed
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
sandbox="allow-scripts allow-same-origin allow-presentation"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>Payment Widget
<iframe
src="https://payment.stripe.com/widget"
sandbox="allow-scripts allow-forms allow-same-origin"
></iframe>Static Content
<!-- No JavaScript needed -->
<iframe
src="https://docs.example.com/page"
sandbox="allow-same-origin"
></iframe>Completely Untrusted
<!-- Maximum restrictions -->
<iframe
src="https://completely-untrusted.com"
sandbox=""
></iframe>Feature Policy (Permissions Policy)
Control browser features iframe can access.
<iframe
src="https://widget.com"
allow="
geolocation 'none';
microphone 'none';
camera 'none';
payment 'self' https://payment.trusted.com;
"
></iframe>Common Features
accelerometer
autoplay
camera
display-capture
encrypted-media
fullscreen
geolocation
gyroscope
magnetometer
microphone
midi
payment
picture-in-picture
usbSyntax
<!-- Allow for all origins -->
<iframe allow="camera *"></iframe>
<!-- Allow for self only -->
<iframe allow="camera 'self'"></iframe>
<!-- Allow for specific origins -->
<iframe allow="camera https://trusted.com"></iframe>
<!-- Block completely -->
<iframe allow="camera 'none'"></iframe>
<!-- Multiple policies -->
<iframe allow="camera 'self'; microphone 'none'; geolocation https://maps.com"></iframe>X-Frame-Options
Prevent your site from being embedded (clickjacking protection).
Server-Side
// Next.js middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-Frame-Options', 'DENY');
// or
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
return response;
}
// Express.js
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});Values
DENY: Cannot be embedded anywhere
SAMEORIGIN: Can be embedded on same origin
ALLOW-FROM https://trusted.com: (Deprecated, don't use)CSP frame-ancestors
Modern alternative to X-Frame-Options.
// More flexible than X-Frame-Options
const csp = `
frame-ancestors 'none'; // Like DENY
`;
// or
const csp = `
frame-ancestors 'self'; // Like SAMEORIGIN
`;
// or
const csp = `
frame-ancestors https://trusted.com https://another-trusted.com;
`;
response.headers.set('Content-Security-Policy', csp);Preventing Clickjacking
Frame Busting (Legacy)
<script>
if (top !== self) {
top.location = self.location;
}
</script>Better: Use X-Frame-Options or CSP frame-ancestors
Modern Approach
// middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Prevent embedding
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set(
'Content-Security-Policy',
"frame-ancestors 'none'"
);
return response;
}Communication with iframes
postMessage (Safe)
// Parent page
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage(
{ type: 'INIT', data: { userId: 123 } },
'https://trusted-widget.com' // Specify target origin!
);
// Listen for responses
window.addEventListener('message', (event) => {
// ✅ Validate origin
if (event.origin !== 'https://trusted-widget.com') {
return;
}
// ✅ Validate message structure
if (event.data?.type === 'READY') {
console.log('Widget is ready');
}
});// Inside iframe
window.addEventListener('message', (event) => {
// ✅ Validate origin
if (event.origin !== 'https://parent-site.com') {
return;
}
// ✅ Validate message
if (event.data?.type === 'INIT') {
const { userId } = event.data.data;
// Send response
event.source.postMessage(
{ type: 'READY' },
event.origin
);
}
});Security Rules for postMessage
// ❌ DANGEROUS
window.addEventListener('message', (event) => {
eval(event.data); // Never!
});
// ❌ DANGEROUS
window.postMessage(message, '*'); // Too permissive!
// ✅ SECURE
window.addEventListener('message', (event) => {
// 1. Check origin
if (event.origin !== 'https://trusted.com') {
return;
}
// 2. Validate data structure
if (!isValidMessage(event.data)) {
return;
}
// 3. Process safely
handleMessage(event.data);
});
// ✅ SECURE
iframe.contentWindow.postMessage(
message,
'https://trusted.com' // Specific origin
);React Implementation
import { useEffect, useRef, useState } from 'react';
interface SafeIframeProps {
src: string;
sandbox?: string[];
allow?: string;
title: string;
}
function SafeIframe({ src, sandbox = [], allow = '', title }: SafeIframeProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
function handleMessage(event: MessageEvent) {
// Validate origin
const allowedOrigins = ['https://trusted.com'];
if (!allowedOrigins.includes(event.origin)) {
console.warn('Message from untrusted origin:', event.origin);
return;
}
// Handle message
console.log('Received message:', event.data);
}
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
const handleLoad = () => {
setLoaded(true);
// Send initialization message
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: 'INIT' },
new URL(src).origin
);
}
};
return (
<iframe
ref={iframeRef}
src={src}
sandbox={sandbox.join(' ')}
allow={allow}
title={title}
onLoad={handleLoad}
style={{
width: '100%',
height: '600px',
border: 'none',
}}
/>
);
}
// Usage
<SafeIframe
src="https://trusted-widget.com"
sandbox={['allow-scripts', 'allow-same-origin']}
allow="camera 'none'; microphone 'none'"
title="Trusted Widget"
/>Security Checklist
Embedding Third-Party Content
✅ Use sandbox attribute
✅ Minimize sandbox permissions
✅ Use allow attribute to restrict features
✅ Specify title for accessibility
✅ Validate postMessage origins
✅ Never use eval() with iframe data
✅ Set referrerpolicy="no-referrer"
✅ Use HTTPS only
Preventing Your Site Being Embedded
✅ Set X-Frame-Options: DENY
✅ Set CSP frame-ancestors 'none'
✅ Both for maximum compatibility
✅ Test in different browsers
✅ Check in production
Testing
describe('iframe Security', () => {
it('should have sandbox attribute', () => {
const iframe = document.querySelector('iframe');
expect(iframe?.getAttribute('sandbox')).toBeTruthy();
});
it('should restrict sensitive permissions', () => {
const iframe = document.querySelector('iframe');
const allow = iframe?.getAttribute('allow') || '';
expect(allow).toContain("camera 'none'");
expect(allow).toContain("microphone 'none'");
});
it('should prevent clickjacking', async () => {
const response = await fetch('/');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
expect(response.headers.get('Content-Security-Policy'))
.toContain("frame-ancestors 'none'");
});
});Common Pitfalls
❌ No sandbox: iframe has full permissions
✅ Always use sandbox with minimal permissions
❌ allow-scripts + allow-same-origin: Defeats sandbox
✅ Avoid this combination for untrusted content
❌ postMessage to '*': Any origin can intercept
✅ Always specify target origin
❌ No origin validation: Accept messages from anyone
✅ Validate event.origin strictly
❌ Only X-Frame-Options: Not supported everywhere
✅ Use both X-Frame-Options and CSP frame-ancestors
iframes are powerful but dangerous—lock them down with sandbox and validate all communication!