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