Skip to content

Commit 7bf677d

Browse files
committed
feat: US-116-B - Make process.binding() throw instead of returning stubs
1 parent 4cc2c8f commit 7bf677d

2 files changed

Lines changed: 52 additions & 54 deletions

File tree

packages/secure-exec-core/src/bridge/process.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -850,34 +850,12 @@ const process: Record<string, unknown> & {
850850
_emit("warning", { message: msg, name: "Warning" });
851851
},
852852

853-
binding(name: string): Record<string, unknown> {
854-
// Return stub implementations for common bindings
855-
const stubs: Record<string, Record<string, unknown>> = {
856-
fs: {},
857-
buffer: {
858-
Buffer: (globalThis as Record<string, unknown>).Buffer,
859-
constants: BUFFER_CONSTANTS,
860-
kMaxLength: BUFFER_MAX_LENGTH,
861-
kStringMaxLength: BUFFER_MAX_STRING_LENGTH,
862-
},
863-
process_wrap: {},
864-
natives: {},
865-
config: {},
866-
uv: { UV_UDP_REUSEADDR: 4 },
867-
constants: {
868-
MAX_LENGTH: BUFFER_MAX_LENGTH,
869-
MAX_STRING_LENGTH: BUFFER_MAX_STRING_LENGTH,
870-
buffer: BUFFER_CONSTANTS,
871-
},
872-
crypto: {},
873-
string_decoder: {},
874-
os: {},
875-
};
876-
return stubs[name] || {};
853+
binding(_name: string): never {
854+
throw new Error("process.binding is not supported in sandbox");
877855
},
878856

879-
_linkedBinding(name: string): Record<string, unknown> {
880-
return (process as unknown as { binding: (name: string) => Record<string, unknown> }).binding(name);
857+
_linkedBinding(_name: string): never {
858+
throw new Error("process._linkedBinding is not supported in sandbox");
881859
},
882860

883861
dlopen(): void {

packages/secure-exec/tests/runtime-driver/node/sandbox-escape.test.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,51 @@ describe("sandbox escape security", () => {
3030
proc = undefined;
3131
});
3232

33-
it("process.binding() returns inert stubs, not real native bindings", async () => {
33+
it("process.binding() throws instead of returning stubs", async () => {
3434
const capture = createConsoleCapture();
3535
proc = createTestNodeRuntime({ onStdio: capture.onStdio });
3636

3737
const result = await proc.exec(`
3838
const results = {};
3939
40-
// process.binding('fs') should not have real native methods
41-
const fsBind = process.binding('fs');
42-
results.fsHasOpen = typeof fsBind.open === 'function';
43-
results.fsHasStat = typeof fsBind.stat === 'function';
44-
results.fsHasRead = typeof fsBind.read === 'function';
45-
results.fsIsEmpty = Object.keys(fsBind).length === 0;
40+
// process.binding('fs') should throw
41+
try {
42+
process.binding('fs');
43+
results.fsThrew = false;
44+
} catch (e) {
45+
results.fsThrew = true;
46+
results.fsMsg = e.message;
47+
}
4648
47-
// process.binding('spawn_sync') should return empty object
48-
const spawnBind = process.binding('spawn_sync');
49-
results.spawnSyncIsEmpty = Object.keys(spawnBind).length === 0;
49+
// process.binding('buffer') should throw
50+
try {
51+
process.binding('buffer');
52+
results.bufferThrew = false;
53+
} catch (e) {
54+
results.bufferThrew = true;
55+
results.bufferMsg = e.message;
56+
}
5057
51-
// process.binding('pipe_wrap') should return empty object
52-
const pipeBind = process.binding('pipe_wrap');
53-
results.pipeIsEmpty = Object.keys(pipeBind).length === 0;
58+
// process._linkedBinding() should also throw
59+
try {
60+
process._linkedBinding('fs');
61+
results.linkedThrew = false;
62+
} catch (e) {
63+
results.linkedThrew = true;
64+
results.linkedMsg = e.message;
65+
}
5466
5567
console.log(JSON.stringify(results));
5668
`);
5769

5870
expect(result.code).toBe(0);
5971
const results = JSON.parse(capture.stdout().trim());
60-
expect(results.fsHasOpen).toBe(false);
61-
expect(results.fsHasStat).toBe(false);
62-
expect(results.fsHasRead).toBe(false);
63-
expect(results.fsIsEmpty).toBe(true);
64-
expect(results.spawnSyncIsEmpty).toBe(true);
65-
expect(results.pipeIsEmpty).toBe(true);
72+
expect(results.fsThrew).toBe(true);
73+
expect(results.fsMsg).toContain("not supported in sandbox");
74+
expect(results.bufferThrew).toBe(true);
75+
expect(results.bufferMsg).toContain("not supported in sandbox");
76+
expect(results.linkedThrew).toBe(true);
77+
expect(results.linkedMsg).toContain("not supported in sandbox");
6678
});
6779

6880
it("process.dlopen() is blocked inside sandbox", async () => {
@@ -101,8 +113,13 @@ describe("sandbox escape security", () => {
101113
const results = {};
102114
103115
// If escape worked, we'd get the host's real require/process
104-
results.hasHostBinding = typeof escaped.process?.binding === 'function'
105-
&& typeof escaped.process.binding('fs')?.open === 'function';
116+
// process.binding should throw in the sandbox — if it returns, that's an escape
117+
try {
118+
const fsBind = escaped.process?.binding?.('fs');
119+
results.hasHostBinding = typeof fsBind?.open === 'function';
120+
} catch {
121+
results.hasHostBinding = false;
122+
}
106123
results.hasDlopen = false;
107124
try {
108125
escaped.process?.dlopen?.({}, '/tmp/fake.node');
@@ -313,9 +330,11 @@ describe("sandbox escape security", () => {
313330
try {
314331
const g = Function('return this')();
315332
if (g !== globalThis) escapes.push('Function-constructor-different-global');
316-
if (typeof g.process?.binding === 'function' &&
317-
typeof g.process.binding('fs')?.open === 'function')
318-
escapes.push('Function-constructor-real-bindings');
333+
try {
334+
const fsBind = g.process?.binding?.('fs');
335+
if (typeof fsBind?.open === 'function')
336+
escapes.push('Function-constructor-real-bindings');
337+
} catch { /* binding throws in sandbox — correct */ }
319338
} catch { /* blocked is fine */ }
320339
321340
// 2. eval-based escape
@@ -336,10 +355,11 @@ describe("sandbox escape security", () => {
336355
const vm = require('vm');
337356
if (typeof vm?.runInThisContext === 'function') {
338357
const g = vm.runInThisContext('this');
339-
// The real escape is if it has real native bindings
340-
if (typeof g?.process?.binding === 'function' &&
341-
typeof g.process.binding('fs')?.open === 'function')
342-
escapes.push('vm-runInThisContext-real-bindings');
358+
try {
359+
const fsBind = g?.process?.binding?.('fs');
360+
if (typeof fsBind?.open === 'function')
361+
escapes.push('vm-runInThisContext-real-bindings');
362+
} catch { /* binding throws in sandbox — correct */ }
343363
}
344364
} catch { /* blocked is fine */ }
345365

0 commit comments

Comments
 (0)