@johnhenry/andbox 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/package.json +1 -1
- package/src/capability-gate.mjs +2 -2
- package/src/index.d.ts +19 -1
- package/src/sandbox.mjs +39 -14
- package/src/worker-source.mjs +51 -10
package/README.md
CHANGED
|
@@ -257,6 +257,7 @@ Creates a new sandboxed runtime. Returns a promise (Worker mode) or object (inli
|
|
|
257
257
|
| `capabilities` | `Record<string, Function>` | `{}` | Host functions callable via `host.call()` (Worker mode) |
|
|
258
258
|
| `defaultTimeoutMs` | `number` | `30000` | Default timeout for `evaluate()` |
|
|
259
259
|
| `baseURL` | `string` | `location.href` | Base URL for relative imports |
|
|
260
|
+
| `allowedImportHosts` | `string[]` | `[]` | Hostnames `sandboxImport()` may load remote `http(s)` modules from (besides `baseURL`'s own host). Import-map targets are host-authored and always allowed. Default: no remote imports. |
|
|
260
261
|
| `policy` | `GatePolicy` | -- | Rate limiting policy |
|
|
261
262
|
| `onConsole` | `(level, ...args) => void` | -- | Console output handler |
|
|
262
263
|
| `globals` | `Record<string, any>` | `{}` | Global variables (inline/data-uri modes) |
|
|
@@ -433,9 +434,9 @@ andbox is **not** a boundary against code that is actively trying to escape it.
|
|
|
433
434
|
|
|
434
435
|
**What is still yours:**
|
|
435
436
|
|
|
436
|
-
- **Worker-global APIs
|
|
437
|
-
- **`sandboxImport()`
|
|
438
|
-
- **A timeout
|
|
437
|
+
- **Worker-global APIs: partly removed, not contained.** Since 0.1.0 the worker prelude deletes `fetch`, `WebSocket`, `WebSocketStream`, `WebTransport`, `EventSource`, `XMLHttpRequest`, `Worker`, `SharedWorker`, `importScripts`, `indexedDB`, `caches`, `BroadcastChannel`, `postMessage` and `self` from the global scope before any evaluated code runs (after the runtime has captured what it needs), shadows those names for evaluated code, and gives evaluated code a throwaway `this`. A script no longer gets them by name, through `globalThis`, indirect `eval` or `Function`. **This is hardening, not a boundary, and a Worker is not a security boundary.** Still reachable: the platform `import()` operator (syntax, it cannot be deleted or shadowed: it can fetch and execute remote code and is an exfiltration channel; `allowedImportHosts` only governs `sandboxImport()`), timing and `SharedArrayBuffer`/`Atomics` side channels, anything the engine or platform adds later that is not on the list above (a deny-list can only ever be incomplete), and under Node `process`, `require` and the rest of the Node API (use `nodeWorker.permissions`, and see [Node](#node)). Everything shares the Worker's realm and heap, so any prototype or intrinsic the code mutates is shared with the runtime. A different isolation primitive is required for hostile code: [`mode: 'wasm'`](#mode-wasm) (QuickJS in WebAssembly, no ambient authority) or a cross-origin iframe with a strict CSP. See [andbox#10](https://github.com/johnhenry/andbox/issues/10).
|
|
438
|
+
- **`sandboxImport()` remote imports are deny-by-default (0.1.0), and only `sandboxImport()` is governed.** An absolute or protocol-relative `http(s)` specifier is refused (`Import denied: <host> is not in allowedImportHosts`) unless its hostname is in `allowedImportHosts` or is `baseURL`'s own host. Import-map targets and virtual modules are host-authored and unaffected. The check cannot see inside a module once loaded (its own static `import`s) and cannot stop the platform `import()` operator (see the previous item). `mode: 'wasm'` never fetches URLs at all. See [andbox#7](https://github.com/johnhenry/andbox/issues/7).
|
|
439
|
+
- **A timeout cannot undo in-flight host-side effects; it can ask them to stop.** When the Worker is terminated (timeout, an aborted `evaluate()`, `dispose()`, a crash) every capability call still in flight sees `this.signal` abort, and its late result is dropped rather than delivered. Cancellation is cooperative: a capability that ignores `this.signal` still runs to completion on the host. Write effectful capabilities as `function`s (not arrows) and pass the signal on (`fetch(url, { signal: this.signal })`), and keep them idempotent. In `mode: 'wasm'`, a cooperative `deadlineMs` ends the evaluation without terminating the Worker, so the signal does not abort in that case. See [andbox#8](https://github.com/johnhenry/andbox/issues/8).
|
|
439
440
|
- **`mode: 'service-worker'` does not provide isolation by merely existing.** It's a hosting mechanism -- a real Service Worker, same-origin by default, serving your `files` map with real fetch/navigation interception. Content served through it can see and touch its own origin exactly like any other same-origin page can; nothing about registering a Service Worker sandboxes what runs inside the pages it serves. If you're hosting content you don't fully trust, point this mode at a genuinely separate origin from day one -- the same recommendation the `fetch`/`WebSocket`/`Worker` item above makes for `worker` mode (a cross-origin iframe with a strict CSP), not something bolted on after the fact. See [andbox#14](https://github.com/johnhenry/andbox/issues/14).
|
|
440
441
|
- **The Service Worker does not control the very first navigation into its scope.** A page/iframe navigation into `scope` that happens *before* the registration has finished activating is a normal, unintercepted network request -- Service Workers never retroactively intercept a request that already went out. `createSandbox({ mode: 'service-worker' })`'s returned promise only resolves once the registration is active (its generated script also calls `clients.claim()` on activate, which helps *already-open* clients but not fresh navigations); the documented, load-bearing contract is: don't navigate anything into `scope` until that promise resolves. Do that and every request is intercepted from the first byte, because the registration already matches `scope` before the navigation request is made. See [andbox#14](https://github.com/johnhenry/andbox/issues/14).
|
|
441
442
|
|
|
@@ -448,9 +449,9 @@ What each mode is built to stop, and what it is not. "Hostile" means code active
|
|
|
448
449
|
| | `worker` / `node-worker` | `wasm` |
|
|
449
450
|
|---|---|---|
|
|
450
451
|
| **Runs in** | The Worker's own JS engine (`new Function`) | QuickJS-ng compiled to WebAssembly, inside the Worker / worker thread |
|
|
451
|
-
| **Reaching `fetch`, `WebSocket`, `importScripts`, `indexedDB`, `postMessage`** |
|
|
452
|
-
| **Forging protocol messages to the host** |
|
|
453
|
-
| **`sandboxImport` of arbitrary URLs / Node builtins** |
|
|
452
|
+
| **Reaching `fetch`, `WebSocket`, `importScripts`, `indexedDB`, `postMessage`** | Removed from the global scope by the prelude (0.1.0), so not reachable by name or via `globalThis`/`eval`/`Function`. Deny-list hardening only: `import()`, timing channels and (Node) `process`/`require` remain. | Not possible. The engine has no such globals, and `constructor`/`eval`/`Function` chains only reach the guest realm. |
|
|
453
|
+
| **Forging protocol messages to the host** | `postMessage`/`self` are removed; ids are random UUIDs and each `result` must echo a per-evaluate nonce. | Not possible. The guest has no `postMessage` or `self`. |
|
|
454
|
+
| **`sandboxImport` of arbitrary URLs / Node builtins** | Remote `http(s)` URLs refused unless listed in `allowedImportHosts`; the raw `import()` operator is still unrestricted (browser), and Node builtins are blocked only with `nodeWorker.permissions` (Node). | Refused: only virtual modules resolve; no URL is fetched. |
|
|
454
455
|
| **Prototype-chain names via `host.call`** | Closed by the capability gate (`Object.create(null)`). | Same gate, plus the guest never sees host objects. |
|
|
455
456
|
| **Infinite loops** | `terminate()` after `timeoutMs`, then a new Worker. | Deterministic `fuel` and a wall-clock `deadlineMs` stop it without a respawn; `terminate()` remains the backstop. |
|
|
456
457
|
| **Memory exhaustion** | Browser: nothing but the tab limit. Node: opt-in `nodeWorker.maxMemoryMb`. | Guest heap cap plus a hard cap on the engine's linear memory. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnhenry/andbox",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sandboxed JavaScript runtime with Worker isolation, RPC, import maps, timeouts, and an optional QuickJS-in-WebAssembly mode",
|
|
6
6
|
"main": "./src/index.mjs",
|
package/src/capability-gate.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function gateCapabilities(capabilities, policy = {}) {
|
|
|
49
49
|
for (const [name, fn] of Object.entries(capabilities)) {
|
|
50
50
|
const capLimits = { ...DEFAULT_CAPABILITY_LIMITS, ...(hasOwn(capPolicies, name) ? capPolicies[name] : undefined) };
|
|
51
51
|
|
|
52
|
-
gated[name] = async (...args)
|
|
52
|
+
gated[name] = async function (...args) {
|
|
53
53
|
// Measure argument bytes
|
|
54
54
|
const argStr = JSON.stringify(args);
|
|
55
55
|
const argBytes = new TextEncoder().encode(argStr).byteLength;
|
|
@@ -82,7 +82,7 @@ export function gateCapabilities(capabilities, policy = {}) {
|
|
|
82
82
|
concurrent++;
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
|
-
return await fn(
|
|
85
|
+
return await fn.apply(this, args);
|
|
86
86
|
} finally {
|
|
87
87
|
concurrent--;
|
|
88
88
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -310,12 +310,23 @@ export interface EvaluateOptions {
|
|
|
310
310
|
deadlineMs?: number;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* `this` inside a capability function (use a `function`, not an arrow).
|
|
315
|
+
* `signal` aborts when the sandbox's Worker is terminated (timeout, an
|
|
316
|
+
* aborted `evaluate()`, `dispose()`, crash); cooperative capabilities should
|
|
317
|
+
* pass it to whatever they are doing (`fetch(url, { signal })`, ...).
|
|
318
|
+
*/
|
|
319
|
+
export interface CapabilityContext {
|
|
320
|
+
signal: AbortSignal;
|
|
321
|
+
name: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
313
324
|
/** Options for createSandbox(). */
|
|
314
325
|
export interface SandboxOptions {
|
|
315
326
|
/** Import map for module resolution inside the sandbox. */
|
|
316
327
|
importMap?: ImportMap;
|
|
317
328
|
/** Host functions callable from sandbox code via host.call(name, ...args). */
|
|
318
|
-
capabilities?: Record<string, (...args: any[]) => any>;
|
|
329
|
+
capabilities?: Record<string, (this: CapabilityContext, ...args: any[]) => any>;
|
|
319
330
|
/** Default timeout in milliseconds for evaluate() calls. */
|
|
320
331
|
defaultTimeoutMs?: number;
|
|
321
332
|
/** Base URL for resolving relative imports inside the sandbox. */
|
|
@@ -386,6 +397,13 @@ export interface SandboxOptions {
|
|
|
386
397
|
* a live sandbox keeps the process alive until dispose().
|
|
387
398
|
*/
|
|
388
399
|
unref?: boolean;
|
|
400
|
+
/**
|
|
401
|
+
* Hostnames `sandboxImport()` may load remote http(s) modules from, in
|
|
402
|
+
* addition to the host of `baseURL`. Default `[]`: remote imports are
|
|
403
|
+
* refused. Import-map targets and virtual modules are not affected. Does
|
|
404
|
+
* not restrict the platform `import()` operator in worker mode.
|
|
405
|
+
*/
|
|
406
|
+
allowedImportHosts?: string[];
|
|
389
407
|
}
|
|
390
408
|
|
|
391
409
|
/** Options for the built-in Node worker_threads mode (`nodeWorker`). */
|
package/src/sandbox.mjs
CHANGED
|
@@ -402,8 +402,14 @@ async function createWorkerSandbox(options = {}, forceNode = false, isWasm = fal
|
|
|
402
402
|
onConsole,
|
|
403
403
|
nodeWorker,
|
|
404
404
|
unref = false,
|
|
405
|
+
allowedImportHosts = [],
|
|
405
406
|
} = options;
|
|
406
407
|
|
|
408
|
+
if (!Array.isArray(allowedImportHosts) || !allowedImportHosts.every((h) => typeof h === 'string')) {
|
|
409
|
+
throw new TypeError('allowedImportHosts must be an array of hostname strings');
|
|
410
|
+
}
|
|
411
|
+
const importHosts = allowedImportHosts.map((h) => h.toLowerCase());
|
|
412
|
+
|
|
407
413
|
// Node mode: no global Worker (and no blob: worker URLs) -> node:worker_threads.
|
|
408
414
|
// An explicit workerFactory always wins; 'node-worker' forces Node; the
|
|
409
415
|
// default selects it only when there is no global Worker under Node.
|
|
@@ -446,6 +452,8 @@ async function createWorkerSandbox(options = {}, forceNode = false, isWasm = fal
|
|
|
446
452
|
let disposed = false;
|
|
447
453
|
let worker = null;
|
|
448
454
|
let workerBlobURL = null;
|
|
455
|
+
// Lifetime of the current Worker; aborted when it is terminated.
|
|
456
|
+
let workerAbort = null;
|
|
449
457
|
|
|
450
458
|
// `unref`: the thread only keeps the host process alive while work is in
|
|
451
459
|
// flight (startup, evaluate, defineModule); idle, it lets the process exit.
|
|
@@ -467,6 +475,7 @@ async function createWorkerSandbox(options = {}, forceNode = false, isWasm = fal
|
|
|
467
475
|
// ── Worker lifecycle ──
|
|
468
476
|
|
|
469
477
|
function createWorker() {
|
|
478
|
+
workerAbort = new AbortController();
|
|
470
479
|
const source = isWasm ? makeWasmWorkerSource() : makeWorkerSource();
|
|
471
480
|
if (workerFactory) {
|
|
472
481
|
worker = workerFactory(source);
|
|
@@ -558,6 +567,7 @@ async function createWorkerSandbox(options = {}, forceNode = false, isWasm = fal
|
|
|
558
567
|
type: 'configure',
|
|
559
568
|
importMap,
|
|
560
569
|
baseURL,
|
|
570
|
+
allowedImportHosts: importHosts,
|
|
561
571
|
virtualModules: Object.fromEntries(virtualModules),
|
|
562
572
|
...(wasmConfig ? { wasm: { ...wasmConfig.engine, memoryBytes: wasmConfig.limits.memoryBytes } } : {}),
|
|
563
573
|
});
|
|
@@ -570,30 +580,45 @@ async function createWorkerSandbox(options = {}, forceNode = false, isWasm = fal
|
|
|
570
580
|
}
|
|
571
581
|
|
|
572
582
|
async function handleCapabilityCall(rpcId, name, args) {
|
|
583
|
+
// Bind to the worker that made the call. If it is killed (timeout, abort,
|
|
584
|
+
// dispose, crash) while the capability is still running, the late result
|
|
585
|
+
// must be dropped: `worker` is then null or a *different* replacement
|
|
586
|
+
// thread that never asked for it (andbox#30).
|
|
587
|
+
const callerWorker = worker;
|
|
588
|
+
const callerSignal = workerAbort?.signal;
|
|
589
|
+
if (!callerWorker || !callerSignal) return;
|
|
590
|
+
const reply = (payload) => {
|
|
591
|
+
if (worker !== callerWorker || callerSignal.aborted) return;
|
|
592
|
+
try {
|
|
593
|
+
callerWorker.postMessage({ type: 'capabilityResult', id: rpcId, ...payload });
|
|
594
|
+
} catch {
|
|
595
|
+
// the thread went away between the check and the post: nothing to tell
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
573
599
|
const fn = lookupCapability(name);
|
|
574
600
|
if (!fn) {
|
|
575
|
-
|
|
576
|
-
type: 'capabilityResult',
|
|
577
|
-
id: rpcId,
|
|
578
|
-
success: false,
|
|
579
|
-
error: `Unknown capability: ${name}`,
|
|
580
|
-
});
|
|
601
|
+
reply({ success: false, error: `Unknown capability: ${name}` });
|
|
581
602
|
return;
|
|
582
603
|
}
|
|
583
604
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
605
|
+
// The signal travels as `this`, not as an argument, so capabilities
|
|
606
|
+
// with their own arity (Math.max, rest params, ...) are unaffected.
|
|
607
|
+
// It aborts when this Worker is terminated (andbox#8).
|
|
608
|
+
const value = await fn.call({ signal: callerSignal, name }, ...args);
|
|
609
|
+
reply({ success: true, value });
|
|
586
610
|
} catch (e) {
|
|
587
|
-
|
|
588
|
-
type: 'capabilityResult',
|
|
589
|
-
id: rpcId,
|
|
590
|
-
success: false,
|
|
591
|
-
error: e.message || String(e),
|
|
592
|
-
});
|
|
611
|
+
reply({ success: false, error: e?.message || String(e) });
|
|
593
612
|
}
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
function terminateWorker() {
|
|
616
|
+
if (workerAbort) {
|
|
617
|
+
// Tell in-flight capability calls their caller is gone so cooperative
|
|
618
|
+
// ones can cancel the underlying effect (andbox#8).
|
|
619
|
+
workerAbort.abort(new Error('Sandbox worker terminated'));
|
|
620
|
+
workerAbort = null;
|
|
621
|
+
}
|
|
597
622
|
if (worker) {
|
|
598
623
|
worker.terminate();
|
|
599
624
|
worker = null;
|
package/src/worker-source.mjs
CHANGED
|
@@ -32,12 +32,49 @@ export function makeWorkerSource() {
|
|
|
32
32
|
// ── State ──
|
|
33
33
|
let importMap = { imports: {}, scopes: {} };
|
|
34
34
|
let baseURL = 'https://andbox.local/';
|
|
35
|
+
let allowedImportHosts = [];
|
|
35
36
|
const virtualModules = new Map();
|
|
36
37
|
// Node mode only: the adapter provides a loader that lets virtual modules
|
|
37
38
|
// import each other. Captured once and removed from the global scope.
|
|
38
39
|
const nodeVirtual = globalThis.__andboxNodeVirtual;
|
|
39
40
|
try { delete globalThis.__andboxNodeVirtual; } catch {}
|
|
40
41
|
|
|
42
|
+
// ── Worker-global lockdown (andbox#10, hardening only) ──
|
|
43
|
+
// Capture what the runtime needs, then remove the ambient network/worker APIs
|
|
44
|
+
// so evaluated code does not get them by name, via globalThis, via indirect
|
|
45
|
+
// eval or via Function. This is NOT a security boundary: the platform \`import()\`
|
|
46
|
+
// operator, Atomics/timing channels and, under Node, \`process\`/\`require\` stay
|
|
47
|
+
// reachable. Use mode: 'wasm' (or an OS-level boundary) for hostile code.
|
|
48
|
+
const scope = self;
|
|
49
|
+
const post = self.postMessage.bind(self);
|
|
50
|
+
const closeSelf = typeof self.close === 'function' ? self.close.bind(self) : () => {};
|
|
51
|
+
const LOCKED_GLOBALS = [
|
|
52
|
+
'fetch', 'XMLHttpRequest', 'WebSocket', 'WebSocketStream', 'WebTransport', 'EventSource',
|
|
53
|
+
'Worker', 'SharedWorker', 'importScripts', 'indexedDB', 'caches', 'BroadcastChannel',
|
|
54
|
+
'postMessage', 'self',
|
|
55
|
+
];
|
|
56
|
+
for (const k of LOCKED_GLOBALS) {
|
|
57
|
+
try { delete globalThis[k]; } catch {}
|
|
58
|
+
if (k in globalThis) {
|
|
59
|
+
try { Object.defineProperty(globalThis, k, { value: undefined, writable: false, configurable: false }); } catch {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Names shadowed lexically for evaluated code as well (covers environments
|
|
63
|
+
// where a global could not be deleted).
|
|
64
|
+
const SHADOWED = [...LOCKED_GLOBALS, 'window'];
|
|
65
|
+
|
|
66
|
+
// ── Remote import policy (andbox#7) ──
|
|
67
|
+
function assertImportAllowed(href) {
|
|
68
|
+
let u;
|
|
69
|
+
try { u = new URL(href); } catch { return; }
|
|
70
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
|
|
71
|
+
const host = u.hostname.toLowerCase();
|
|
72
|
+
let baseHost = '';
|
|
73
|
+
try { baseHost = new URL(baseURL).hostname.toLowerCase(); } catch {}
|
|
74
|
+
if (host === baseHost || allowedImportHosts.includes(host)) return;
|
|
75
|
+
throw new Error(\`Import denied: \${host} is not in allowedImportHosts\`);
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
// ── Import Map Resolver (inlined) ──
|
|
42
79
|
function resolveWithImportMap(specifier, map, parentURL) {
|
|
43
80
|
if (!map) return null;
|
|
@@ -93,11 +130,13 @@ async function sandboxImport(specifier) {
|
|
|
93
130
|
// 3. Relative/absolute URL — resolve against baseURL
|
|
94
131
|
if (specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/')) {
|
|
95
132
|
const resolved = new URL(specifier, baseURL).href;
|
|
133
|
+
assertImportAllowed(resolved);
|
|
96
134
|
return await import(resolved);
|
|
97
135
|
}
|
|
98
136
|
|
|
99
137
|
// 4. Absolute URL passthrough
|
|
100
138
|
if (specifier.startsWith('http://') || specifier.startsWith('https://')) {
|
|
139
|
+
assertImportAllowed(specifier);
|
|
101
140
|
return await import(specifier);
|
|
102
141
|
}
|
|
103
142
|
|
|
@@ -113,7 +152,7 @@ function callCapability(name, args) {
|
|
|
113
152
|
const id = crypto.randomUUID();
|
|
114
153
|
return new Promise((resolve, reject) => {
|
|
115
154
|
pendingRpc.set(id, { resolve, reject });
|
|
116
|
-
|
|
155
|
+
post({ type: 'capabilityCall', id, name, args });
|
|
117
156
|
});
|
|
118
157
|
}
|
|
119
158
|
|
|
@@ -135,7 +174,7 @@ function makeForwardingConsole(evalId) {
|
|
|
135
174
|
try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
|
|
136
175
|
catch { return String(a); }
|
|
137
176
|
});
|
|
138
|
-
|
|
177
|
+
post({ type: 'console', evalId, level: prop, args: serialized });
|
|
139
178
|
};
|
|
140
179
|
}
|
|
141
180
|
return target[prop];
|
|
@@ -144,23 +183,24 @@ function makeForwardingConsole(evalId) {
|
|
|
144
183
|
}
|
|
145
184
|
|
|
146
185
|
// ── Message Handler ──
|
|
147
|
-
|
|
186
|
+
scope.onmessage = async ({ data: msg }) => {
|
|
148
187
|
switch (msg.type) {
|
|
149
188
|
case 'configure': {
|
|
150
189
|
if (msg.importMap) importMap = msg.importMap;
|
|
151
190
|
if (msg.baseURL) baseURL = msg.baseURL;
|
|
191
|
+
if (Array.isArray(msg.allowedImportHosts)) allowedImportHosts = msg.allowedImportHosts;
|
|
152
192
|
if (msg.virtualModules) {
|
|
153
193
|
for (const [name, src] of Object.entries(msg.virtualModules)) {
|
|
154
194
|
virtualModules.set(name, src);
|
|
155
195
|
}
|
|
156
196
|
}
|
|
157
|
-
|
|
197
|
+
post({ type: 'configured' });
|
|
158
198
|
break;
|
|
159
199
|
}
|
|
160
200
|
|
|
161
201
|
case 'defineModule': {
|
|
162
202
|
virtualModules.set(msg.name, msg.source);
|
|
163
|
-
|
|
203
|
+
post({ type: 'moduleDefined', name: msg.name });
|
|
164
204
|
break;
|
|
165
205
|
}
|
|
166
206
|
|
|
@@ -171,13 +211,14 @@ self.onmessage = async ({ data: msg }) => {
|
|
|
171
211
|
// after the user code matter: without the trailing one, code ending in
|
|
172
212
|
// a // line comment swallows the closing brace (andbox#23).
|
|
173
213
|
const asyncFn = new Function(
|
|
174
|
-
'sandboxImport', 'host', 'console',
|
|
214
|
+
'sandboxImport', 'host', 'console', ...SHADOWED,
|
|
175
215
|
\`return (async () => {\\n\${msg.code}\\n})();\`
|
|
176
216
|
);
|
|
177
|
-
|
|
178
|
-
|
|
217
|
+
// \`this\` is a throwaway object so a bare \`this\` is not the global.
|
|
218
|
+
const result = await asyncFn.call(Object.freeze(Object.create(null)), sandboxImport, host, fwdConsole);
|
|
219
|
+
post({ type: 'result', id: msg.id, nonce: msg.nonce, success: true, value: serialize(result) });
|
|
179
220
|
} catch (e) {
|
|
180
|
-
|
|
221
|
+
post({
|
|
181
222
|
type: 'result',
|
|
182
223
|
id: msg.id,
|
|
183
224
|
nonce: msg.nonce,
|
|
@@ -207,7 +248,7 @@ self.onmessage = async ({ data: msg }) => {
|
|
|
207
248
|
reject(new Error('Sandbox disposed'));
|
|
208
249
|
}
|
|
209
250
|
pendingRpc.clear();
|
|
210
|
-
|
|
251
|
+
closeSelf();
|
|
211
252
|
break;
|
|
212
253
|
}
|
|
213
254
|
}
|