Skip to content

Commit 4a7d105

Browse files
committed
feat: US-114 - Prevent process.env mutations from reaching child processes
1 parent eeb713a commit 4a7d105

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

packages/secure-exec-node/src/bridge-setup.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,13 @@ export async function setupRequire(
479479
}>("child_process.spawn options", optionsJson, jsonPayloadLimit);
480480
const sessionId = nextSessionId++;
481481

482+
// Use init-time filtered env when no explicit env — sandbox
483+
// process.env mutations must not propagate to children
484+
const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
485+
482486
const proc = executor.spawn(command, args, {
483487
cwd: options.cwd,
484-
env: stripDangerousEnv(options.env),
488+
env: childEnv,
485489
onStdout: (data) => {
486490
getDispatchRef().applySync(
487491
undefined,
@@ -559,9 +563,13 @@ export async function setupRequire(
559563
let stderrBytes = 0;
560564
let maxBufferExceeded = false;
561565

566+
// Use init-time filtered env when no explicit env — sandbox
567+
// process.env mutations must not propagate to children
568+
const childEnv = stripDangerousEnv(options.env ?? deps.processConfig.env);
569+
562570
const proc = executor.spawn(command, args, {
563571
cwd: options.cwd,
564-
env: stripDangerousEnv(options.env),
572+
env: childEnv,
565573
onStdout: (data) => {
566574
if (maxBufferExceeded) return;
567575
stdoutBytes += data.length;

packages/secure-exec/tests/runtime-driver/node/env-leakage.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,52 @@ describe("runtime driver specific: node env leakage", () => {
180180
CUSTOM: "value",
181181
});
182182
});
183+
184+
// ---------------------------------------------------------------
185+
// Env isolation — sandbox mutations must not reach children
186+
// ---------------------------------------------------------------
187+
188+
it("sandbox process.env.LD_PRELOAD does not reach child spawned with default env", async () => {
189+
const { executor, calls } = createCapturingExecutor();
190+
proc = createTestNodeRuntime({
191+
permissions: { ...allowAllChildProcess, ...allowAllEnv },
192+
commandExecutor: executor,
193+
processConfig: { env: { SAFE_INIT: "yes" } },
194+
});
195+
await proc.run(`
196+
const cp = require('child_process');
197+
process.env.LD_PRELOAD = '/evil/lib.so';
198+
cp.spawnSync('echo', ['hi']);
199+
`);
200+
expect(calls.length).toBe(1);
201+
// Child gets init-time env, not the mutated sandbox env
202+
expect(calls[0].env).toBeDefined();
203+
expect(calls[0].env!.LD_PRELOAD).toBeUndefined();
204+
expect(calls[0].env!.SAFE_INIT).toBe("yes");
205+
});
206+
207+
it("sandbox process.env.FOO mutation is visible locally but not in child default env", async () => {
208+
const { executor, calls } = createCapturingExecutor();
209+
const capture = createConsoleCapture();
210+
proc = createTestNodeRuntime({
211+
permissions: { ...allowAllChildProcess, ...allowAllEnv },
212+
commandExecutor: executor,
213+
processConfig: { env: { INIT_VAR: "original" } },
214+
onStdio: capture.onStdio,
215+
});
216+
await proc.run(`
217+
const cp = require('child_process');
218+
process.env.FOO = 'bar';
219+
console.log(JSON.stringify({ foo: process.env.FOO }));
220+
cp.spawnSync('echo', ['hi']);
221+
`);
222+
// Sandbox-local mutation works
223+
const parsed = JSON.parse(capture.stdout().trim());
224+
expect(parsed.foo).toBe("bar");
225+
// Child gets init-time env without the mutation
226+
expect(calls.length).toBe(1);
227+
expect(calls[0].env).toBeDefined();
228+
expect(calls[0].env!.FOO).toBeUndefined();
229+
expect(calls[0].env!.INIT_VAR).toBe("original");
230+
});
183231
});

0 commit comments

Comments
 (0)