How Gadgets Are Sandboxed in Cloudflare OS: A Dual-Layer Security Model

Cloudflare OS isolates each Gadget in a dual-layer sandbox that combines client-side iframe restrictions with a hardened server-side Durable Object runtime, ensuring code can only communicate through explicit RPC bindings and cannot access the broader internet.

Cloudflare OS, developed in the cloudflare/cloudflare-os repository, uses a sophisticated Gadget sandboxing architecture to safely execute third-party code within the Cloudflare network. This security model employs two distinct isolation layers—client-side and server-side—that work together to prevent data leakage while enabling rich functionality through controlled capability-based communication.

Client-Side Sandbox: Isolating Gadget UIs in Restricted Iframes

The first layer of protection begins in the browser. When a Gadget’s UI loads, the Workshop injects it into an <iframe> element configured with the sandbox="allow-scripts" attribute. This configuration, documented in packages/workshop-shared/src/api.ts (lines 21-24), explicitly removes capabilities that would allow the iframe to reach the outside world.

By stripping access to native browser APIs such as fetch, alert, and confirm, the sandbox ensures the Gadget cannot directly interact with external services or annoy users with pop-ups. The only permitted communication channel is postMessage(), which the Workshop wraps in a Cap’n Web RPC channel. This creates an RPC stub (exposed as the global gadget object) that points directly to the Gadget’s Durable Object on the server, establishing a type-safe, capability-based communication path.

// Client-side RPC stub injection (packages/workshop-shared/src/api.ts)
import { RpcStub } from "capnweb";

// The Workshop injects this global `gadget` into the sandboxed iframe.
declare const gadget: RpcStub<GadgetServer>;

// Example: call a server method without direct network access
await gadget.doSomething({ foo: "bar" });

Server-Side Sandbox: Restricted Workers Runtime

While the client-side iframe prevents browser-level escapes, the server-side implementation enforces strict runtime boundaries. According to packages/workshop-backend/src/agent.ts (lines 711-713), each Gadget executes in a restricted variant of the Cloudflare Workers runtime that explicitly disables arbitrary internet access.

In this hardened environment, calls to fetch() will fail immediately, preventing the Gadget from exfiltrating data or calling unauthorized external APIs. Instead, the code can only access external services through explicit env bindings defined in the deployment configuration, such as KV storage, Durable Objects, or Gatekeeper capabilities. This binding-only approach limits the attack surface and guarantees that the Gadget operates strictly within its granted permissions.

// Server-side sandbox implementation (packages/workshop-backend/src/agent.ts)
export class GadgetServer implements RpcTarget {
  // `env` only contains the bindings the deployment declared.
  constructor(private readonly env: Env) {}

  async doSomething(input: { foo: string }) {
    // ❌ This will throw because fetch is disabled in the sandbox.
    // await fetch("https://example.com"); // <-- Illegal
    return { result: `Hello ${input.foo}` };
  }
}

Gatekeeper UI Sandboxing

The sandboxing model extends beyond user Gadgets to include Gatekeeper management apps—administrative interfaces for services like Context Library, Email, and Cloudflare itself. As implemented in packages/workshop-shared/src/gatekeeper.ts (lines 409-416), these configuration UIs also run inside sandboxed iframes with the same sandbox="allow-scripts" restriction.

Each Gatekeeper iframe receives a ui capability over a MessagePort RPC session, allowing the third-party UI to interact with the Workshop through a well-defined interface without gaining unrestricted access to the parent context. This ensures that management applications remain isolated while still capable of performing privileged operations through explicit capability delegation.

// Gatekeeper UI capability exposure (packages/workshop-shared/src/gatekeeper.ts)
export interface GatekeeperUiFrame {
  /** HTML of the UI, served inside a sandboxed iframe. */
  html: string;
  /** Capability the UI receives to talk back to the Workshop. */
  ui: RpcStub<GatekeeperUi>;
}

Runtime Enforcement Mechanisms

Cloudflare OS enforces these sandboxing policies through three complementary enforcement points that operate at different stages of the execution lifecycle:

  • Content Security Policy Headers: The packages/workshop-backend/src/browser-export.ts file (lines 35-44) sets the Content-Security-Policy: sandbox allow-scripts header on iframe responses, ensuring browser-level sandbox enforcement regardless of iframe attributes.

  • Origin Isolation: The build process defined in scripts/build-gatekeeper-configurator.ts compiles worker code without the allow-same-origin flag, ensuring each sandboxed realm starts with an empty principal and cannot access cookies or storage from other origins.

  • Type-Safe RPC Boundaries: All RPC interfaces crossing the sandbox boundary are annotated with @validateRpc(), enforcing runtime type checking on all data exchanged between the isolated Gadget and the host system.

Summary

  • Dual-layer isolation: Cloudflare OS combines client-side iframe sandboxing (sandbox="allow-scripts") with a server-side restricted Workers runtime to create defense-in-depth.
  • Binding-only network access: Server-side Gadgets cannot use fetch() for arbitrary internet requests; they must use explicitly declared env bindings.
  • Capability-based communication: All cross-boundary communication occurs through type-safe Cap’n Web RPC channels, with no direct access to browser APIs like alert, confirm, or fetch.
  • Universal application: The same sandboxing rules apply to both user Gadgets and Gatekeeper management UIs, ensuring consistent security across the platform.

Frequently Asked Questions

What prevents a Gadget from making arbitrary network requests?

The server-side sandbox in packages/workshop-backend/src/agent.ts explicitly disables the fetch() API within the restricted Workers runtime. While the client-side iframe also blocks fetch via the sandbox attribute, the server-side enforcement is absolute—any attempt to call fetch() throws an exception. Gadgets can only access network resources through pre-configured env bindings such as KV stores or Durable Objects.

How does the client-side iframe communicate with the server?

Communication occurs exclusively through postMessage() wrapped in a Cap’n Web RPC channel. The Workshop injects a global gadget RPC stub into the iframe that points to the Gadget's server-side Durable Object. This stub allows type-safe method calls without exposing the underlying network transport or browser APIs.

Are Gatekeeper management apps subject to the same sandboxing as Gadgets?

Yes. According to packages/workshop-shared/src/gatekeeper.ts, Gatekeeper apps run in sandboxed iframes with identical sandbox="allow-scripts" restrictions. They receive a ui capability over a MessagePort RPC session, maintaining isolation while enabling administrative functionality through explicit capabilities.

What happens if a Gadget tries to access browser APIs like alert or confirm?

The sandbox="allow-scripts" attribute on the Gadget's iframe explicitly removes access to modal dialogs and other privileged browser APIs. If the code attempts to call window.alert() or window.confirm(), the browser will throw a security error because these features are not included in the sandbox's allowlist.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →