Front-end Engineering Lab

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
usb

Syntax

<!-- 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!

On this page