@@ -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