|
1 | 1 | import ivm from "isolated-vm"; |
2 | 2 | import { afterEach, describe, expect, it } from "vitest"; |
3 | | -import { allowAllFs, allowAllChildProcess, createInMemoryFileSystem, createDefaultNetworkAdapter } from "../../../src/index.js"; |
| 3 | +import { allowAllFs, allowAllChildProcess, allowAllNetwork, createInMemoryFileSystem, createDefaultNetworkAdapter } from "../../../src/index.js"; |
4 | 4 | import type { NodeRuntime } from "../../../src/index.js"; |
5 | 5 | import { createTestNodeRuntime } from "../../test-utils.js"; |
6 | 6 |
|
@@ -362,6 +362,85 @@ describe("bridge-side resource hardening", () => { |
362 | 362 | }); |
363 | 363 | }); |
364 | 364 |
|
| 365 | + // ------------------------------------------------------------------- |
| 366 | + // HTTP server ownership — close only servers created in this context |
| 367 | + // ------------------------------------------------------------------- |
| 368 | + |
| 369 | + describe("HTTP server ownership", () => { |
| 370 | + it("sandbox can close a server it created", async () => { |
| 371 | + const adapter = createDefaultNetworkAdapter(); |
| 372 | + const capture = createConsoleCapture(); |
| 373 | + |
| 374 | + proc = createTestNodeRuntime({ |
| 375 | + permissions: { ...allowAllNetwork }, |
| 376 | + networkAdapter: adapter, |
| 377 | + onStdio: capture.onStdio, |
| 378 | + }); |
| 379 | + |
| 380 | + const result = await proc.exec(` |
| 381 | + const http = require('http'); |
| 382 | + const server = http.createServer((req, res) => { |
| 383 | + res.writeHead(200); |
| 384 | + res.end('ok'); |
| 385 | + }); |
| 386 | +
|
| 387 | + (async () => { |
| 388 | + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); |
| 389 | + await new Promise((resolve, reject) => { |
| 390 | + server.close((err) => err ? reject(err) : resolve()); |
| 391 | + }); |
| 392 | + console.log('close:ok'); |
| 393 | + })(); |
| 394 | + `); |
| 395 | + |
| 396 | + expect(result.code).toBe(0); |
| 397 | + expect(capture.stdout()).toContain("close:ok"); |
| 398 | + }); |
| 399 | + |
| 400 | + it("sandbox cannot close a server it did not create", async () => { |
| 401 | + const adapter = createDefaultNetworkAdapter(); |
| 402 | + const capture = createConsoleCapture(); |
| 403 | + |
| 404 | + // Pre-register a server in the adapter that was NOT created by this context |
| 405 | + await adapter.httpServerListen!({ |
| 406 | + serverId: 42, |
| 407 | + port: 0, |
| 408 | + hostname: "127.0.0.1", |
| 409 | + onRequest: async () => ({ status: 200 }), |
| 410 | + }); |
| 411 | + |
| 412 | + proc = createTestNodeRuntime({ |
| 413 | + permissions: { ...allowAllNetwork }, |
| 414 | + networkAdapter: adapter, |
| 415 | + onStdio: capture.onStdio, |
| 416 | + }); |
| 417 | + |
| 418 | + const result = await proc.exec(` |
| 419 | + const http = require('http'); |
| 420 | +
|
| 421 | + (async () => { |
| 422 | + try { |
| 423 | + // Attempt to call the host bridge reference directly with an unowned serverId |
| 424 | + await _networkHttpServerCloseRaw.apply( |
| 425 | + undefined, [42], { result: { promise: true } } |
| 426 | + ); |
| 427 | + console.log('close:unexpected'); |
| 428 | + } catch (e) { |
| 429 | + console.log('close:denied'); |
| 430 | + console.log('error:' + e.message); |
| 431 | + } |
| 432 | + })(); |
| 433 | + `); |
| 434 | + |
| 435 | + expect(capture.stdout()).toContain("close:denied"); |
| 436 | + expect(capture.stdout()).toContain("not owned by this execution context"); |
| 437 | + expect(capture.stdout()).not.toContain("close:unexpected"); |
| 438 | + |
| 439 | + // Clean up the externally-created server |
| 440 | + await adapter.httpServerClose!(42); |
| 441 | + }); |
| 442 | + }); |
| 443 | + |
365 | 444 | // ------------------------------------------------------------------- |
366 | 445 | // Module cache isolation across __unsafeCreateContext calls |
367 | 446 | // ------------------------------------------------------------------- |
|
0 commit comments