Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
997c7df07a [autofix.ci] apply automated fixes 2025-09-07 12:26:27 +00:00
Claude Bot
c3fac090e7 Harden process spawning on Windows
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 12:24:11 +00:00
4 changed files with 236 additions and 0 deletions

View File

@@ -1066,6 +1066,7 @@ pub const WindowsSpawnOptions = struct {
verbatim_arguments: bool = false,
hide_window: bool = true,
loop: jsc.EventLoopHandle = undefined,
shell_enabled: bool = false,
};
pub const Stdio = union(enum) {
@@ -1528,6 +1529,39 @@ pub fn spawnProcessPosix(
unreachable;
}
fn isWindowsBatchFile(path: []const u8) bool {
if (path.len < 4) return false;
// Find the last dot to get the extension
var i = path.len;
while (i > 0) : (i -= 1) {
if (path[i - 1] == '.') break;
}
if (i == 0) return false;
const ext = path[i - 1 ..];
// Check for .bat or .cmd extensions (case-insensitive)
if (ext.len == 4) {
// .bat
if ((ext[1] == 'b' or ext[1] == 'B') and
(ext[2] == 'a' or ext[2] == 'A') and
(ext[3] == 't' or ext[3] == 'T'))
{
return true;
}
// .cmd
if ((ext[1] == 'c' or ext[1] == 'C') and
(ext[2] == 'm' or ext[2] == 'M') and
(ext[3] == 'd' or ext[3] == 'D'))
{
return true;
}
}
return false;
}
pub fn spawnProcessWindows(
options: *const WindowsSpawnOptions,
argv: [*:null]?[*:0]const u8,
@@ -1541,6 +1575,13 @@ pub fn spawnProcessWindows(
uv_process_options.args = argv;
uv_process_options.env = envp;
uv_process_options.file = options.argv0 orelse argv[0].?;
// Security check: prevent direct execution of batch files without shell
// This prevents command injection vulnerabilities (similar to CVE-2024-27980)
const file_path = std.mem.sliceTo(uv_process_options.file, 0);
if (isWindowsBatchFile(file_path) and !options.windows.shell_enabled) {
return .{ .err = bun.sys.Error.fromCode(.INVAL, .uv_spawn) };
}
uv_process_options.exit_cb = &Process.onExitUV;
var stack_allocator = std.heap.stackFallback(8192, bun.default_allocator);
const allocator = stack_allocator.get();

View File

@@ -1028,6 +1028,7 @@ pub fn spawnMaybeSync(
var windows_hide: bool = false;
var windows_verbatim_arguments: bool = false;
var windows_shell_enabled: bool = false;
var abort_signal: ?*jsc.WebCore.AbortSignal = null;
defer {
// Ensure we clean it up on error.
@@ -1214,6 +1215,12 @@ pub fn spawnMaybeSync(
windows_verbatim_arguments = val.asBoolean();
}
}
if (try args.get(globalThis, "windowsShellEnabled")) |val| {
if (val.isBoolean()) {
windows_shell_enabled = val.asBoolean();
}
}
}
if (try args.get(globalThis, "timeout")) |timeout_value| brk: {
@@ -1376,6 +1383,7 @@ pub fn spawnMaybeSync(
.windows = if (Environment.isWindows) .{
.hide_window = windows_hide,
.verbatim_arguments = windows_verbatim_arguments,
.shell_enabled = windows_shell_enabled,
.loop = jsc.EventLoopHandle.init(jsc_vm),
},
};

View File

@@ -1007,6 +1007,7 @@ function normalizeSpawnArguments(file, args, options) {
file,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!windowsVerbatimArguments,
windowsShellEnabled: !!options.shell,
argv0: options.argv0,
};
}
@@ -1333,6 +1334,7 @@ class ChildProcess extends EventEmitter {
cwd: options.cwd || undefined,
env: env,
detached: typeof detachedOption !== "undefined" ? !!detachedOption : false,
windowsShellEnabled: !!options.windowsShellEnabled,
onExit: (handle, exitCode, signalCode, err) => {
this.#handle = handle;
this.pid = this.#handle.pid;

View File

@@ -0,0 +1,185 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, tempDir } from "harness";
import { exec, execSync, spawn, spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
describe("Batch file execution security on Windows", () => {
// Only run these tests on Windows
if (process.platform !== "win32") {
test.skip("Windows-only test", () => {});
return;
}
test("should prevent direct execution of .bat files without shell option", () => {
using dir = tempDir("batch-security");
const batFile = join(String(dir), "test.bat");
writeFileSync(batFile, "@echo test output");
// This should throw an error
expect(() => {
spawnSync(batFile, [], { env: bunEnv });
}).toThrow();
// Try with spawn (async)
const child = spawn(batFile, [], { env: bunEnv });
return new Promise((resolve, reject) => {
child.on("error", err => {
expect(err.code).toBe("EINVAL");
resolve();
});
child.on("exit", () => {
reject(new Error("Process should not have executed"));
});
});
});
test("should prevent direct execution of .cmd files without shell option", () => {
using dir = tempDir("batch-security");
const cmdFile = join(String(dir), "test.cmd");
writeFileSync(cmdFile, "@echo test output");
// This should throw an error
expect(() => {
spawnSync(cmdFile, [], { env: bunEnv });
}).toThrow();
// Try with spawn (async)
const child = spawn(cmdFile, [], { env: bunEnv });
return new Promise((resolve, reject) => {
child.on("error", err => {
expect(err.code).toBe("EINVAL");
resolve();
});
child.on("exit", () => {
reject(new Error("Process should not have executed"));
});
});
});
test("should allow execution of .bat files with shell: true", () => {
using dir = tempDir("batch-security");
const batFile = join(String(dir), "test.bat");
writeFileSync(batFile, "@echo test output");
// This should work
const result = spawnSync(batFile, [], {
shell: true,
encoding: "utf8",
env: bunEnv,
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("test output");
});
test("should allow execution of .cmd files with shell: true", () => {
using dir = tempDir("batch-security");
const cmdFile = join(String(dir), "test.cmd");
writeFileSync(cmdFile, "@echo test output");
// This should work
const result = spawnSync(cmdFile, [], {
shell: true,
encoding: "utf8",
env: bunEnv,
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("test output");
});
test("should prevent command injection in batch file arguments without shell", () => {
using dir = tempDir("batch-security");
const batFile = join(String(dir), "test.bat");
writeFileSync(batFile, "@echo %1");
// This should throw an error (batch files can't be executed without shell)
expect(() => {
spawnSync(batFile, ["&calc.exe"], { env: bunEnv });
}).toThrow();
// Also test with quotes
expect(() => {
spawnSync(batFile, ['"&calc.exe'], { env: bunEnv });
}).toThrow();
});
test("exec and execSync should work with batch files (they use shell by default)", () => {
using dir = tempDir("batch-security");
const batFile = join(String(dir), "test.bat");
writeFileSync(batFile, "@echo exec test");
// execSync uses shell by default
const result = execSync(`"${batFile}"`, {
encoding: "utf8",
env: bunEnv,
});
expect(result).toContain("exec test");
// exec uses shell by default
return new Promise((resolve, reject) => {
exec(`"${batFile}"`, { env: bunEnv }, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
expect(stdout).toContain("exec test");
resolve();
}
});
});
});
test("should handle case-insensitive batch file extensions", () => {
using dir = tempDir("batch-security");
const extensions = [".BAT", ".bAt", ".BaT", ".CMD", ".cMd", ".CmD"];
for (const ext of extensions) {
const file = join(String(dir), `test${ext}`);
writeFileSync(file, "@echo test");
// Should throw without shell
expect(() => {
spawnSync(file, [], { env: bunEnv });
}).toThrow();
// Should work with shell
const result = spawnSync(file, [], {
shell: true,
encoding: "utf8",
env: bunEnv,
});
expect(result.status).toBe(0);
}
});
test("should allow normal executables without shell", () => {
// Test that normal executables still work
const result = spawnSync("cmd.exe", ["/c", "echo", "test"], {
encoding: "utf8",
env: bunEnv,
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("test");
});
test("should check the actual file being executed, not arguments", () => {
using dir = tempDir("batch-security");
const batFile = join(String(dir), "test.bat");
writeFileSync(batFile, "@echo test");
// Even if we have .bat in arguments, should work if the executable is not a batch file
const result = spawnSync("cmd.exe", ["/c", "echo", "test.bat"], {
encoding: "utf8",
env: bunEnv,
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("test.bat");
});
});