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/.
1. The minimum directives by tier
Section titled “1. The minimum directives by tier”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.
Tiers 1–2 (link or quote widget)
Section titled “Tiers 1–2 (link or quote widget)”script-src 'self' https://expedition.insure;frame-src https://expedition.insure;script-src 'self' https://expedition.insure— lets your page loadwidget.jsfrom our origin. ('self'covers your own scripts; add our origin alongside.)frame-src https://expedition.insure— lets the iframewidget.jsinjects point athttps://expedition.insure/embed/quote. Usechild-srcinstead if you target older browsers that predateframe-src.
Tier 3 (embedded checkout)
Section titled “Tier 3 (embedded checkout)”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.comin yourframe-srcbecause 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.
2. Why connect-src is not needed
Section titled “2. Why connect-src is not needed”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.comThe header is resolved from the per-operator Origin allowlist tied to your
pk_op_.... Two outcomes:
| Your page origin | frame-ancestors resolves to | Result |
|---|---|---|
| On the allowlist | your 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 copiedpkembedded 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-ancestorsCSP 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.
The sandbox we set
Section titled “The sandbox we set”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"| Token | Why |
|---|---|
allow-scripts | Runs the quote app (and Stripe.js in Tier 3) inside the iframe. |
allow-same-origin | The 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-forms | Quote form and Stripe Elements submission. |
allow-popups | Provider / 3DS flows that open a tab. |
allow-top-navigation-by-user-activation | Lets the Buy → hosted-checkout handoff navigate the top window — only on a user click, never silently. |
What a strict outer sandbox can break
Section titled “What a strict outer sandbox can break”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.
5. The ei:navigate top-nav fallback
Section titled “5. The ei:navigate top-nav fallback”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 navThe 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:navigateyourself. 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 keepallow-top-navigation-by-user-activationin any outer sandbox if you can; if you can’t, the fallback covers it automatically.
6. Putting it together
Section titled “6. Putting it together”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.comChecklist:
-
https://expedition.insureis in yourscript-src(sowidget.jsloads). -
https://expedition.insureis in yourframe-src(so the iframe renders). - Tier 3 only:
https://js.stripe.comis in yourframe-src. - Your embed origin is on our allowlist (so
frame-ancestorsresolves to it, not'none'). - You did not add
https://expedition.insuretoconnect-src— it isn’t needed. - Any outer sandbox preserves
allow-scripts,allow-same-origin,allow-forms, andallow-popups; ideally alsoallow-top-navigation-by-user-activation(else theei:navigatefallback handles the handoff).
Next steps
Section titled “Next steps”- Embed integration guide — install, config params, events, and the key/origin exchange.
- /reference/embed-api/ — the embed HTTP API, event payloads, and CORS contract in full.
- /operate/reconciliation/ — go-live: the
policy.issuedwebhook, attribution, and commissions.