Skip to content

CSP and iframe sandbox cookbook

This page is the security-header cookbook for getting the Expedition Insure quote widget to render under a Content-Security-Policy (CSP) — what directives to add on your page, what sandbox tokens we set on the iframe, and how our fail-closed frame-ancestors model works. It’s for the engineer wiring the widget into a site that runs a strict CSP or nests it inside another sandbox.

If you haven’t mounted the widget yet, start at the embedded-insurance integration guide; for the raw HTTP contracts see /reference/embed-api/; for go-live operations see /operate/reconciliation/.

The widget loads a script (widget.js) and frames a child document on our origin, https://expedition.insure. Your page’s CSP must allow both. The exact set depends on the tier you’re running.

script-src 'self' https://expedition.insure;
frame-src https://expedition.insure;
  • script-src 'self' https://expedition.insure — lets your page load widget.js from our origin. ('self' covers your own scripts; add our origin alongside.)
  • frame-src https://expedition.insure — lets the iframe widget.js injects point at https://expedition.insure/embed/quote. Use child-src instead if you target older browsers that predate frame-src.

Tier 3 confirms the insurance PaymentIntent inside the iframe with Stripe.js, which itself frames https://js.stripe.com. Add it to frame-src:

script-src 'self' https://expedition.insure;
frame-src https://expedition.insure https://js.stripe.com;

You frame us; we frame Stripe. You only need https://js.stripe.com in your frame-src because the Stripe frame is a grandchild of your page. You never load Stripe.js yourself and never handle card data — it runs on our origin inside the checkout iframe.

The embed HTTP API (POST /api/embed/quote, GET /api/embed/options) is called by the code running inside our iframe, on our origin — not by your page. Those fetch/XHR requests are governed by our CSP, not yours. So you do not add https://expedition.insure to your connect-src, and you do not relax it for the widget at all.

The same goes for the postMessage bridge: window.postMessage between the iframe and your page is not a network request and is not subject to connect-src. The only directives the widget touches on your side are script-src and frame-src.

3. Our fail-closed frame-ancestors (server side)

Section titled “3. Our fail-closed frame-ancestors (server side)”

You control whether the iframe is allowed to frame our origin (frame-src). We independently control whether our document is willing to be framed by you — via a frame-ancestors directive we set on every /embed/* response, resolved per-request from your publishable key (pk).

GET https://expedition.insure/embed/quote?pk=pk_op_...&host=https://www.youroperator.com
→ Content-Security-Policy: frame-ancestors https://www.youroperator.com

The header is resolved from the per-operator Origin allowlist tied to your pk_op_.... Two outcomes:

Your page originframe-ancestors resolves toResult
On the allowlistyour allowlisted origin(s)iframe renders
Not on the allowlist'none'browser refuses to render — blank frame

Fail-closed on our side. An unknown or non-allowlisted origin resolves to frame-ancestors 'none', not to a permissive default. A copied pk embedded on a page we haven’t allowlisted produces a blank frame, never data. This is enforced by the browser from our header — it does not depend on your CSP.

To get an origin allowlisted, send us the exact origins (scheme + host + optional port — no path, query, or fragment) you’ll embed from. Up to 20 origins per operator. The same allowlist also gates the embed API’s CORS: a non-allowlisted Origin gets a bare 403 with no CORS headers, so the browser blocks the response body even when one is present.

Blank frame? Check the console. A frame-ancestors CSP violation in your browser console confirms the origin isn’t allowlisted. Send us the exact origin string from the violation and we’ll add it.

4. Nested sandbox and strict-CSP host pages

Section titled “4. Nested sandbox and strict-CSP host pages”

If you embed our widget inside your own sandbox (a tag manager iframe, a CMS block, a third-party container), your outer sandbox can strip capabilities from our inner iframe before they ever reach it. Two things to know.

widget.js sets this exact sandbox attribute on the iframe — you don’t set it, but your security review should know what to expect:

sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation-by-user-activation"
TokenWhy
allow-scriptsRuns the quote app (and Stripe.js in Tier 3) inside the iframe.
allow-same-originThe framed document is our own origin, so same-origin is safe here; required for Stripe.js and for the same-origin top-nav handoff.
allow-formsQuote form and Stripe Elements submission.
allow-popupsProvider / 3DS flows that open a tab.
allow-top-navigation-by-user-activationLets the Buy → hosted-checkout handoff navigate the top window — only on a user click, never silently.

If your page wraps the widget in an outer sandbox that does not include allow-top-navigation-by-user-activation, that capability is stripped from our inner iframe. The Buy → hosted-checkout handoff would otherwise have no way to move the top window to our /options page. The widget handles this with a postMessage fallback — see §5.

For the iframe to function at all, an outer sandbox should preserve allow-scripts, allow-same-origin, allow-forms, and allow-popups. Stripping allow-scripts or allow-same-origin will prevent the quote app (and Tier-3 checkout) from running.

When a traveler clicks Buy, the widget hands off to our hosted /options page by navigating the top window. It tries two paths, in order:

1. Direct top-nav → iframe sets window.top.location to the /options URL
(works because the iframe is same-origin and has
allow-top-navigation-by-user-activation)
2. Fallback → if direct top-nav is blocked by a stricter outer sandbox,
the iframe posts { type: "ei:navigate", url } to your page;
the loader (which holds real top access) performs the nav

The loader re-validates the fallback before acting: the url must be an absolute URL whose origin equals our embed origin (https://expedition.insure). A compromised or misbehaving iframe can never bounce your top window to an arbitrary site — the postMessage origin is already locked to our embed origin, and the URL is re-checked as a second control.

You don’t wire ei:navigate yourself. It’s an internal handoff between the iframe and the loader — it is not re-emitted to your .on() handlers. The only thing you need to do is keep allow-top-navigation-by-user-activation in any outer sandbox if you can; if you can’t, the fallback covers it automatically.

A complete CSP fragment for a Tier-3 host page that runs the widget and lets the hosted-checkout handoff work:

Content-Security-Policy:
script-src 'self' https://expedition.insure;
frame-src https://expedition.insure https://js.stripe.com;

And, on our side, automatically per request:

Content-Security-Policy: frame-ancestors https://www.youroperator.com

Checklist:

  • https://expedition.insure is in your script-src (so widget.js loads).
  • https://expedition.insure is in your frame-src (so the iframe renders).
  • Tier 3 only: https://js.stripe.com is in your frame-src.
  • Your embed origin is on our allowlist (so frame-ancestors resolves to it, not 'none').
  • You did not add https://expedition.insure to connect-src — it isn’t needed.
  • Any outer sandbox preserves allow-scripts, allow-same-origin, allow-forms, and allow-popups; ideally also allow-top-navigation-by-user-activation (else the ei:navigate fallback handles the handoff).