| @@ -135,17 +135,26 @@ test("sandboxed runner does not expose host globals", async () => { · 134 unmodified lines |
| 135 | | 135 | |
| 136 | const result = await runCodeWithAdapter( | 136 | const result = await runCodeWithAdapter( |
| 137 | request(` | 137 | request(` |
| | 138 | let fsEscape = "unknown"; |
| | 139 | try { |
| | 140 | fsEscape = [].constructor.constructor("return typeof process")(); |
| | 141 | } catch { |
| | 142 | fsEscape = "blocked"; |
| | 143 | } |
| | 144 | |
| 138 | const checks = { | 145 | const checks = { |
| 139 | process: typeof process, | 146 | process: typeof process, |
| 140 | bun: typeof Bun, | 147 | bun: typeof Bun, |
| 141 | fetch: typeof fetch, | 148 | fetch: typeof fetch, |
| 142 | fsEscape: [].constructor.constructor("return typeof process")(), | 149 | fsEscape, |
| 143 | }; | 150 | }; |
| 144 | console.log(JSON.stringify(checks)); | 151 | console.log(JSON.stringify(checks)); |
| 145 | if (checks.process !== "undefined") throw new Error("process leaked"); | 152 | if (checks.process !== "undefined") throw new Error("process leaked"); |
| 146 | if (checks.bun !== "undefined") throw new Error("Bun leaked"); | 153 | if (checks.bun !== "undefined") throw new Error("Bun leaked"); |
| 147 | if (checks.fetch !== "undefined") throw new Error("fetch leaked"); | 154 | 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 | } |
| 149 | return "ok"; | 158 | return "ok"; |
| 150 | `), | 159 | `), |
| 151 | adapter, | 160 | adapter, |
| @@ -155,7 +164,65 @@ test("sandboxed runner does not expose host globals", async () => { · 3 unmodified lines |
| 155 | expect(result.stdout).toContain("\"process\":\"undefined\""); | 164 | expect(result.stdout).toContain("\"process\":\"undefined\""); |
| 156 | expect(result.stdout).toContain("\"bun\":\"undefined\""); | 165 | expect(result.stdout).toContain("\"bun\":\"undefined\""); |
| 157 | expect(result.stdout).toContain("\"fetch\":\"undefined\""); | 166 | 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(); |
| 159 | }); | 226 | }); |
| 160 | | 227 | |
| 161 | test("runs openapi and graphql sourced tools through the runtime", async () => { | 228 | test("runs openapi and graphql sourced tools through the runtime", async () => { |