History

Enhance sandbox security and testing capabilities

- Updated tests in `runtime-core.test.ts` to improve checks against global constructor and eval escapes, ensuring that sensitive host globals are not exposed. - Modified `runCodeWithAdapter` in `runtime-core.ts` and `vercel-sandbox-runtime.ts` to utilize a sandboxed context with stricter code generation settings, enhancing security during code execution. - Added new tests to verify that prototype pollution does not leak to the host environment, further strengthening the sandbox's isolation features.

Rhys Sullivan committed 9 months ago

Commit fe78c871 parent 70e652d3 files · +84 −5Browse files

Unified · Split

3 files
  1. executor/apps/server/src/runtimes/runtime-core.test.ts +70 −3
  2. executor/apps/server/src/runtimes/runtime-core.ts +7 −1
  3. executor/apps/server/src/runtimes/vercel-sandbox-runtime.ts +7 −1
executor/apps/server/src/runtimes/runtime-core.test.ts+70−3
@@ -135,17 +135,26 @@ test("sandboxed runner does not expose host globals", async () => { · 134 unmodified lines
135135
136136 const result = await runCodeWithAdapter(
137137 request(`
138+ let fsEscape = "unknown";
139+ try {
140+ fsEscape = [].constructor.constructor("return typeof process")();
141+ } catch {
142+ fsEscape = "blocked";
143+ }
144+
138145 const checks = {
139146 process: typeof process,
140147 bun: typeof Bun,
141148 fetch: typeof fetch,
142− fsEscape: [].constructor.constructor("return typeof process")(),
149+ fsEscape,
143150 };
144151 console.log(JSON.stringify(checks));
145152 if (checks.process !== "undefined") throw new Error("process leaked");
146153 if (checks.bun !== "undefined") throw new Error("Bun leaked");
147154 if (checks.fetch !== "undefined") throw new Error("fetch leaked");
148− if (checks.fsEscape !== "undefined") throw new Error("constructor escape leaked");
155+ if (checks.fsEscape !== "undefined" && checks.fsEscape !== "blocked") {
156+ throw new Error("constructor escape leaked");
157+ }
149158 return "ok";
150159 `),
151160 adapter,
@@ -155,7 +164,65 @@ test("sandboxed runner does not expose host globals", async () => { · 3 unmodified lines
155164 expect(result.stdout).toContain("\"process\":\"undefined\"");
156165 expect(result.stdout).toContain("\"bun\":\"undefined\"");
157166 expect(result.stdout).toContain("\"fetch\":\"undefined\"");
158− expect(result.stdout).toContain("\"fsEscape\":\"undefined\"");
167+ expect(result.stdout).toMatch(/"fsEscape":"(undefined|blocked)"/);
168+});
169+
170+test("sandbox blocks global constructor and eval escapes", async () => {
171+ const adapter = createRuntimeAdapter(new Map(), []);
172+
173+ const result = await runCodeWithAdapter(
174+ request(`
175+ let ctorEscape = "unknown";
176+ let evalEscape = "unknown";
177+ let functionEscape = "unknown";
178+
179+ try {
180+ ctorEscape = globalThis.constructor.constructor("return typeof process")();
181+ } catch {
182+ ctorEscape = "blocked";
183+ }
184+
185+ try {
186+ evalEscape = eval("typeof process");
187+ } catch {
188+ evalEscape = "blocked";
189+ }
190+
191+ try {
192+ functionEscape = Function("return typeof process")();
193+ } catch {
194+ functionEscape = "blocked";
195+ }
196+
197+ console.log(JSON.stringify({ ctorEscape, evalEscape, functionEscape }));
198+ if (ctorEscape !== "undefined" && ctorEscape !== "blocked") throw new Error("ctor escape");
199+ if (evalEscape !== "undefined" && evalEscape !== "blocked") throw new Error("eval escape");
200+ if (functionEscape !== "undefined" && functionEscape !== "blocked") throw new Error("Function escape");
201+ `),
202+ adapter,
203+ );
204+
205+ expect(result.status).toBe("completed");
206+ expect(result.stdout).toMatch(/"ctorEscape":"(undefined|blocked)"/);
207+ expect(result.stdout).toMatch(/"evalEscape":"(undefined|blocked)"/);
208+ expect(result.stdout).toMatch(/"functionEscape":"(undefined|blocked)"/);
209+});
210+
211+test("sandbox prototype pollution does not leak to host", async () => {
212+ const marker = `__pwned_${crypto.randomUUID().replace(/-/g, "")}`;
213+ expect(({} as Record<string, unknown>)[marker]).toBeUndefined();
214+
215+ const adapter = createRuntimeAdapter(new Map(), []);
216+ const result = await runCodeWithAdapter(
217+ request(`
218+ Object.prototype[${JSON.stringify(marker)}] = "yes";
219+ return "done";
220+ `),
221+ adapter,
222+ );
223+
224+ expect(result.status).toBe("completed");
225+ expect(({} as Record<string, unknown>)[marker]).toBeUndefined();
159226 });
160227
161228 test("runs openapi and graphql sourced tools through the runtime", async () => {
executor/apps/server/src/runtimes/runtime-core.ts+7−1
@@ -104,12 +104,18 @@ export async function runCodeWithAdapter( · 103 unmodified lines
104104 error: (...args: unknown[]) => appendStderr(formatArgs(args)),
105105 };
106106
107− const context = createContext({
107+ const sandbox = Object.assign(Object.create(null), {
108108 tools,
109109 console: consoleProxy,
110110 setTimeout,
111111 clearTimeout,
112112 });
113+ const context = createContext(sandbox, {
114+ codeGeneration: {
115+ strings: false,
116+ wasm: false,
117+ },
118+ });
113119
114120 const runnerScript = new Script(`(async () => {\n"use strict";\n${request.code}\n})()`);
115121
executor/apps/server/src/runtimes/vercel-sandbox-runtime.ts+7−1
@@ -146,12 +146,18 @@ const consoleProxy = { · 145 unmodified lines
146146 error: (...args) => appendStderr(formatArgs(args)),
147147 };
148148
149−const context = vm.createContext({
149+const sandbox = Object.assign(Object.create(null), {
150150 tools,
151151 console: consoleProxy,
152152 setTimeout,
153153 clearTimeout,
154154 });
155+const context = vm.createContext(sandbox, {
156+ codeGeneration: {
157+ strings: false,
158+ wasm: false,
159+ },
160+});
155161 const runnerScript = new vm.Script("(async () => {\\n\"use strict\";\\n" + userCode + "\\n})()");
156162
157163 const timeoutPromise = new Promise((_, reject) => {