Skip to content

Iframes Are a Security Nightmare (Here's How to Survive Them)

Published: at 10:15 AMSuggest Changes
3 min read

Iframes are ancient, messy, and somehow still everywhere. Maps, payment widgets, comment sections, embedded videos - half the web runs inside little boxes inside bigger boxes. The problem is that every iframe you embed is essentially a window into someone else’s code running on your domain. That’s not great.

Here’s what I’ve learned about keeping iframes under control without losing the functionality you actually need.

The Sandbox Attribute Is Non-Negotiable

If your iframe doesn’t have a sandbox attribute, fix that now. A bare sandbox applies maximum restrictions. You add back only what the embedded content actually needs:

<iframe
  src="https://third-party-widget.com"
  sandbox="allow-scripts allow-forms"
></iframe>

Common permissions you’ll actually use:

  • allow-scripts - lets JavaScript run
  • allow-forms - allows form submission
  • allow-same-origin - keeps the original origin (be careful with this one)
  • allow-popups - lets the iframe open new windows
  • allow-popups-to-escape-sandbox - popups get fewer restrictions than the parent iframe

For a commenting widget, you might need:

<iframe
  src="https://comments-widget.com"
  sandbox="allow-scripts allow-forms allow-popups-to-escape-sandbox allow-same-origin"
></iframe>

Start with nothing. Add permissions one by one when things break. That’s the whole philosophy.

CSP: Don’t Trust the Iframe, Don’t Let It Trust You

Your server should send headers that say who can embed what:

Content-Security-Policy: frame-src https://trusted-domain.com https://*.approved-service.net;

This only allows iframes from specific domains. You can also control who embeds your content:

Content-Security-Policy:
  frame-src https://trusted-domain.com;
  frame-ancestors 'self' https://parent-allowed.com;

If you don’t set frame-ancestors, you’re one sketchy embed away from a clickjacking attack.

Permissions Policy: Lock Down Browser APIs

Even sandboxed iframes can request camera, microphone, or geolocation access if you let them. Don’t let them:

<iframe
  src="https://mapping-service.com"
  allow="geolocation 'self'; camera 'none'"
></iframe>

Or set it globally as a header:

Permissions-Policy: geolocation=(self "https://mapping-service.com"), camera=()

Default-deny is your friend here.

postMessage: Verify Everything

When you need the parent page and iframe to talk, use postMessage. But verify the origin on every single message. Never use * as the target origin in production.

// Parent sending to iframe
const frame = document.getElementById("external-widget");
frame.contentWindow.postMessage(
  { action: "initialize", theme: "dark" },
  "https://widget-provider.com"
);

// Parent receiving from iframe
window.addEventListener("message", event => {
  if (event.origin !== "https://widget-provider.com") return;

  if (event.data.type === "resize") {
    frame.height = event.data.height;
  }
});

Three rules: specify the target origin, validate the sender, reject anything you don’t recognize.

Common Scenarios

Payment gateway:

<iframe
  id="payment-frame"
  src="https://secure-payments.com/checkout?order=12345"
  sandbox="allow-scripts allow-forms allow-same-origin"
  allow="payment"
></iframe>

Keep payment data out of your parent window entirely. Let the iframe handle it.

Embedded video:

<iframe
  src="https://video-platform.com/embed/v123456"
  sandbox="allow-scripts allow-same-origin"
  allow="fullscreen; encrypted-media; picture-in-picture"
  loading="lazy"
></iframe>

Lazy loading matters more than you’d think for page performance.

Interactive maps:

<iframe
  src="https://maps-service.com/embed?location=paris"
  sandbox="allow-scripts allow-same-origin"
  allow="geolocation"
  referrerpolicy="no-referrer-when-downgrade"
></iframe>

Alternatives Worth Considering

If you control the source code, Web Components are often better than iframes. You get style isolation via Shadow DOM without the security overhead of a full browsing context.

For high-security scenarios, fetch the external content server-side, sanitize it, and render it yourself. No cross-origin headaches at all.

The Checklist

Before shipping any iframe integration:

  • Sandbox is applied with minimal permissions
  • CSP frame-src restricts embeddable origins
  • frame-ancestors prevents unauthorized embedding of your own content
  • Permissions Policy blocks unnecessary browser APIs
  • postMessage handlers validate origin before acting
  • HTTPS only for embedded content
  • Referrer policy is intentional, not accidental

Further Reading