Skip to main content
@enclave-vm/browser implements defense-in-depth through a double iframe architecture. Each execution creates two nested iframes — an outer security barrier and an inner sandbox — with 8 distinct security layers that work together to prevent sandbox escapes.

Double Iframe Architecture

  • Host Page: Your application. Creates the outer iframe and handles tool calls.
  • Outer Iframe: Security barrier with rate limiting, pattern detection, and name filtering. Relays validated messages between host and inner iframe.
  • Inner Iframe: The actual sandbox. Contains prototype hardening, secure proxies, safe runtime wrappers, and the user code.

The 8 Security Layers

Layer 1: Content Security Policy

Both iframes are created with a strict CSP via <meta> tag:
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'
DirectiveEffect
default-src 'none'Blocks all network requests (fetch, XHR, WebSocket, images, fonts)
script-src 'unsafe-inline'Allows inline <script> but blocks eval(), new Function(), setTimeout(string)
No 'unsafe-eval'Equivalent to Node.js VM codeGeneration: { strings: false, wasm: false }

Layer 2: Iframe Sandbox Attribute

Both iframes use a restrictive sandbox attribute:
sandbox="allow-scripts"
Only allow-scripts is enabled. Crucially, allow-same-origin is not set, which means:
  • The iframe cannot access the host page’s DOM, cookies, or storage
  • The iframe cannot read parent.document or top.document
  • The iframe has a unique null origin

Layer 3: Outer Frame Validation

The outer iframe acts as a security barrier between the host and the inner sandbox. All tool calls from the inner iframe pass through validation:
CheckDescription
Rate limitingMax operations per second (default: 100)
Name validationTool names must be non-empty strings
Whitelist patternOptional regex to allow only specific tool names
Blacklist patternsMultiple regexes to block specific tool names
Suspicious pattern detectionDetects multi-step attack sequences (exfiltration, credential theft, etc.)
Rapid enumerationDetects the same operation being called repeatedly (default threshold: 30 in 5s)

Layer 4: Dangerous Global Removal

Dangerous globals are deleted from the inner iframe’s window object based on the security level:
GlobalSTRICTSECURESTANDARDPERMISSIVE
Function, evalRemovedRemovedRemovedAvailable
globalThisRemovedRemovedAvailableAvailable
Proxy, ReflectRemovedRemovedAvailableAvailable
SharedArrayBuffer, AtomicsRemovedRemovedRemovedRemoved
WebAssemblyRemovedRemovedRemovedRemoved
WeakRef, FinalizationRegistryRemovedRemovedRemovedAvailable
ShadowRealmRemovedRemovedRemovedRemoved
Iterator, AsyncIteratorRemovedRemovedAvailableAvailable
performance, TemporalRemovedAvailableAvailableAvailable
Always removed (all security levels): fetch, XMLHttpRequest, WebSocket, EventSource, Worker, SharedWorker, ServiceWorker, importScripts, localStorage, sessionStorage, indexedDB, caches, navigator, open, close, alert, confirm, prompt, document

Layer 5: Prototype Freezing

All built-in prototypes are frozen after security patches are applied:
  • Object.prototype, Array.prototype, Function.prototype
  • String.prototype, Number.prototype, Boolean.prototype
  • Date.prototype, Error.prototype, Promise.prototype
  • TypeError.prototype, RangeError.prototype, SyntaxError.prototype
  • ReferenceError.prototype, URIError.prototype, EvalError.prototype
Legacy accessor methods (__lookupGetter__, __lookupSetter__, __defineGetter__, __defineSetter__) are replaced with no-ops on Object.prototype. Error prototypes have __proto__ shadowed to return null.

Layer 6: Secure Proxy

Every global object exposed to user code is wrapped in a Proxy that blocks access to dangerous properties:
Blocked PropertyPurpose
__proto__Prevents prototype chain manipulation
prototypePrevents constructor prototype access
constructorPrevents access to native constructors
__defineGetter__Prevents legacy accessor injection
__defineSetter__Prevents legacy accessor injection
__lookupGetter__Prevents legacy accessor introspection
__lookupSetter__Prevents legacy accessor introspection
The proxy behavior is configurable per security level. At STRICT and SECURE levels, accessing blocked properties throws an error. At PERMISSIVE, it returns undefined. Additionally, dangerous static methods on Object are neutralized: defineProperty, defineProperties, setPrototypeOf, getOwnPropertyDescriptor, and getOwnPropertyDescriptors.

Layer 7: Safe Runtime Wrappers

User code runs through transformed wrappers that enforce resource limits:
WrapperPurpose
callTool(name, args)Tool calls with count limits and JSON sanitization
__safe_forTraditional for loops with iteration limits
__safe_whileWhile loops with iteration limits
__safe_doWhileDo-while loops with iteration limits
__safe_forOfFor-of loops with iteration limits (generator-based)
__safe_concatSafe string/number concatenation
__safe_templateSafe template literal handling
parallel(fns)Parallel execution of up to 100 total operations with configurable concurrency (default 10, max 20)
consoleConsole relay with call count and byte limits
All loop wrappers share a global iteration counter and check for abort signals on every iteration.

Layer 8: Memory Tracking

Memory-intensive operations are patched to track estimated usage:
MethodProtection
String.prototype.repeatEstimates length * count * 2 bytes before execution
Array.prototype.joinEstimates total string output before execution
Array.prototype.fillEstimates fillCount * 8 bytes before execution
When the cumulative tracked memory exceeds memoryLimit (default: 1MB), a RangeError is thrown.
Memory tracking in the browser is estimation-based. Unlike Node.js where V8 heap statistics provide precise measurements, the browser version tracks only known high-risk operations. It is a defense against obvious memory bombs, not an exact memory budget.

Message Protocol

All communication between layers uses postMessage with validated messages. Every message includes a __enclave_msg__: true discriminator and a requestId for correlation.

Message Types

DirectionTypeDescription
Inner/Outer to Hosttool-callTool invocation with toolName, args, callId
Host to Inner/Outertool-responseTool result or error with callId
Inner to HostresultExecution result with success, value/error, stats
Inner to HostconsoleConsole output with level and args
Outer to HostreadyOuter iframe initialized
Host to InnerabortSignal to abort execution
All messages are validated with Zod schemas. Tool names must match ^[a-zA-Z][a-zA-Z0-9:_-]*$ and be 1–256 characters.

What’s Blocked

Attack VectorProtection
Network requestsCSP default-src 'none' blocks fetch, XHR, WebSocket
eval / new FunctionCSP blocks all forms of code generation from strings
DOM accessdocument shadowed; no allow-same-origin on sandbox
Parent frame accessSandbox prevents reading parent.document or top.document
Prototype pollutionAll prototypes frozen; __proto__ access blocked by proxy
Infinite loopsAll loops wrapped with iteration counters and abort checks
Tool call floodingRate limiting in outer frame + per-execution call limits
Data exfiltrationSuspicious pattern detection in outer frame
Memory bombsString/Array operation tracking with configurable limits
Storage accesslocalStorage, sessionStorage, indexedDB, caches removed

What’s Available

Sandboxed code has access to a safe subset of JavaScript:
// Standard operations
const arr = [1, 2, 3, 4, 5];
const doubled = arr.map(x => x * 2);
const sum = doubled.reduce((a, b) => a + b, 0);

// Tool calls
const users = await callTool('users:list', { limit: 10 });
const filtered = users.filter(u => u.active);

// Parallel execution
const [posts, comments] = await parallel([
  () => callTool('posts:list', {}),
  () => callTool('comments:list', {}),
]);

// Console output (relayed to host)
console.log('Found', filtered.length, 'active users');

// Standard globals
const now = new Date();
const pattern = new RegExp('^test');
const encoded = JSON.stringify({ result: sum });

// Math operations
const random = Math.floor(Math.random() * 100);