---
type: explanation
title: How Caputchin sandboxes games
summary: "Every isolation measure around a Caputchin game: the opaque-origin iframe, its single sandbox token, the strict inline Content-Security-Policy, integrity hashing, and how all of that coexists with a CSP you set on your own page."
---

A Caputchin game is third-party code that runs in your visitor's browser, on your page. Whether it comes from the marketplace or you host it yourself, Caputchin treats it as **untrusted by default** and wraps it in several independent isolation layers, so a buggy or hostile game can render, play, and report a result, but can reach nothing else: not your page, not your visitor's data, not the network.

This page explains every measure, why each exists, and how the whole thing interacts with a CSP you add to your own site.

## The threat model in one line

The game runs arbitrary JavaScript (and possibly WebAssembly) supplied by a third party. The isolation goal is therefore: **the game can compute and draw, and it can hand a result back to the widget, but it cannot read or affect anything outside its own frame.** Every layer below serves that one goal, and they are defence in depth, no single layer is trusted to be perfect.

```mermaid
flowchart TD
  P["Your page (your origin)"] --> W["Caputchin widget (your origin)"]
  W -->|builds the iframe| F["Sandboxed iframe (null origin)"]
  F --> G["The game: JS / WASM"]
  G -->|postMessage result only| W
  subgraph Inside the frame
    G
    L1["sandbox=allow-scripts only"]
    L2["inline Content-Security-Policy"]
    L3["Subresource Integrity (marketplace)"]
  end
```

## Layer 1: an opaque-origin sandboxed iframe

Every game runs inside an `<iframe>` the widget builds with a deliberately minimal `sandbox` attribute. The **only** token granted is `allow-scripts`, because the game is JavaScript and must run. Everything else is withheld, and the most important omission is `allow-same-origin`.

Without `allow-same-origin` the browser gives the frame a unique, **null origin**. That single fact is the load-bearing boundary:

- The game cannot read your page's DOM, cookies, `localStorage`, or the Caputchin session, it is sealed into a throwaway origin that is foreign to yours.
- Because the frame can also never navigate your top window, open popups, submit forms, or pop native dialogs (all of those capabilities are sandbox tokens Caputchin does not grant), there is no path from the game back out to your page.

The combination of **`allow-scripts` without `allow-same-origin`** is the whole point: the browser executes the game's code but treats the frame as a foreign origin that can touch nothing of yours. Caputchin never adds `allow-same-origin` to a game frame. (As a consequence, every message the game posts to the widget arrives from origin `"null"`, which the widget's channel checks expect, a non-null origin would itself signal a misconfigured sandbox.)

## Layer 2: the frame's document is built inline, not fetched

The widget does not point the iframe at a remote page. It constructs the frame's entire HTML document **inline** (via `srcdoc`) and hands it to the null-origin frame. That document is tiny: a strict CSP meta tag (next layer), a small Caputchin runtime bootstrap, and one `<script>` tag that loads the game bundle.

This is the same mechanism for both delivery paths, only the bundle URL differs:

| Game source | Bundle URL in the inline document |
|---|---|
| Marketplace (platform-resolved) | The pinned, integrity-hashed bundle URL Caputchin resolved at mount. |
| Self-hosted (your `game-src`) | The URL you supplied. |

Because the document is authored by the widget rather than the game, the game never controls the CSP, the runtime, or the frame's structure, only what its own bundle does once loaded.

## Layer 3: Subresource Integrity for marketplace games

When Caputchin serves a marketplace game it pins the bundle to an immutable version and records a cryptographic hash (SHA-384) of it. The `<script>` tag that loads the bundle carries that hash as an `integrity` attribute with `crossorigin="anonymous"`, so the **browser itself** refuses to execute the bundle if a single byte differs from what was pinned. A compromised CDN cannot substitute different code; the load fails closed.

A self-hosted game has no platform-asserted hash (you are the trust root for your own bytes), so the `integrity` attribute is simply omitted for that path. See [the replay contract](/docs/marketplace-game-development/replay-contract) for how a marketplace game's *result* is independently re-derived on the server, a separate guarantee from integrity.

## Layer 4: a strict inline Content-Security-Policy

The inline document carries a tight `Content-Security-Policy`. Even though the frame is already null-origin and sandboxed, the CSP further constrains what the loaded code may do, it denies everything by default and re-grants only the minimum:

| Directive | Value | Why |
|---|---|---|
| `default-src` | `'none'` | Deny everything not explicitly allowed below. |
| `script-src` | a hash of Caputchin's own inline runtime + the game bundle's origin + `'wasm-unsafe-eval'` | Run only Caputchin's exact bootstrap (pinned by a `sha256-` hash, **not** `'unsafe-inline'`) and the game's own bundle. `'wasm-unsafe-eval'` lets a WASM engine compile **without** granting full `'unsafe-eval'`. |
| `connect-src` | `'none'` | **The anti-exfiltration directive.** The game cannot `fetch`, `XHR`, open a `WebSocket`, or beacon anywhere. No network egress at all. |
| `img-src` | `data:` (plus any skin-asset origins, below) | Sprites inline, or from an allowed asset host. |
| `media-src` | `data:` (plus any skin-asset origins, below) | Audio/video inline, or from an allowed asset host. |
| `font-src` | `data:` | Inline fonts only. |
| `style-src` | `'unsafe-inline'` | Games set inline styles; no external stylesheets. (Styles cannot exfiltrate data the way script or network can.) |

The net effect: the game runs, renders, and reports its result through the SDK bridge, and can reach nothing else, especially not the network. This CSP is set by Caputchin and is not something you configure; it is listed here so you understand exactly how confined a game is.

### The one place a customer setting widens the frame CSP

Skins can point image or audio fields at an absolute URL on your own CDN (see [skins](/docs/game-customization/skin)). For those assets to load inside the locked-down frame, Caputchin adds **only those exact asset origins** to the frame's `img-src` / `media-src`, and nowhere else. `script-src` and `connect-src` are never widened, so even an allowed asset origin cannot be used to load code or open a network channel. This is the single, narrow way a configured value affects the frame's policy, and it is still asset-only.

## Layer 5: a one-way result channel

The game has no callable surface into your page. The only way out is the SDK bridge: the game calls a single method that `postMessage`s its opaque result (its trace) to the widget, which lives on your origin. The widget is what talks to Caputchin's API; the game never does (it cannot, `connect-src` is `'none'`). So the trust path is game → widget → your backend, and each hop only ever passes a result forward, never grants the game reach backward.

## The CSP you set on your own page [#the-csp-you-set-on-your-own-page]

The layers above protect *you and your visitor from the game*. A Content-Security-Policy on your own page is a different, complementary control: it protects *your page from everything*, and Caputchin is designed to run under a strict one. You do not have to relax your CSP to embed Caputchin; you have to **allow the few origins the widget legitimately uses.**

At minimum, the widget needs:

| Directive | Allow | Why |
|---|---|---|
| `script-src` | the origin you load the widget script from, plus `'unsafe-eval'` | The `<script>` that defines `<caputchin-widget>` / `<caputchin-game>`, the [CDN](/docs/integration-guides/cdn) you chose (jsDelivr, your own host, or `caputchin.com`). `'unsafe-eval'` lets the [instrumentation](/docs/understanding-caputchin/instrumentation) challenge run; it is required while instrumentation is on, and you can drop it by turning instrumentation off on the key's [Security](/docs/site-keys/security) page. |
| `connect-src` | `https://verify.caputchin.com` | The widget calls the Caputchin verification service to set up and confirm a verification. Allow this exact origin. If you point the widget at a different API host, allow that instead. |
| `frame-src` | `https://caputchin.com` | Governs the game iframe. Because the frame uses an inline `srcdoc` document, browsers apply your `frame-src` to it, so allow the Caputchin origin here. |
| `worker-src` | `blob:` | The proof-of-work solver runs entirely in Web Workers created from a `blob:` URL, with no main-thread fallback, so this is required. If you omit `worker-src`, browsers fall back to `child-src` then `default-src`, and `default-src 'self'` alone does not permit `blob:`. |

Optionally add `'wasm-unsafe-eval'` to `script-src`: it lets the proof-of-work solver run as fast WebAssembly. It is not required, the solver falls back to a slower pure-JavaScript implementation (still inside the Worker) without it.

You do **not** need to allow the game's own bundle origin (jsDelivr, a game's CDN, your `game-src` host) in your page CSP: that bundle loads *inside the sandboxed frame*, under the frame's own CSP described above, not under your page's policy. Your page only frames Caputchin; Caputchin confines what runs inside.

If a Caputchin element silently fails to load under your CSP, your browser console names the blocked directive; add the origin or keyword it names to that directive. Start strict and open exactly what the console asks for. You never need `'unsafe-inline'`. You do need `blob:` for workers (`worker-src`) and, while instrumentation is on, `'unsafe-eval'` in `script-src`; the `'unsafe-eval'` requirement goes away if you turn instrumentation off on the key's [Security](/docs/site-keys/security) page. `'wasm-unsafe-eval'` is optional (it speeds up the proof-of-work solver, which otherwise runs slower in JavaScript inside the same Worker).

## Why so many layers

Each layer would be a meaningful boundary on its own; together they mean a failure in one is not a breach. The null origin alone already walls the game off from your data; the CSP alone already kills network egress; integrity alone already pins the exact bytes. Caputchin runs all of them because untrusted third-party code is exactly the place where defence in depth earns its keep.

## See also

- [How games resist bots](/docs/understanding-caputchin/gaming-anti-cheat): the server-authoritative side of game integrity.
- [The replay contract](/docs/marketplace-game-development/replay-contract): how a game's result is independently re-derived server-side.
- [Build a self-hosted game](/docs/custom-game-development/self-hosted-game): the bundle constraints these sandboxes impose on authors.
- [Load the widget from a CDN](/docs/integration-guides/cdn): choosing the script origin your page CSP must allow.
