mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 15:20:45 +00:00
fix(scripts): avoid DEP0190 when spawning .cmd files on Windows
Use the shared Windows cmd.exe command-line builder for `.cmd` and `.bat` UI runner launches so Node.js v24 no longer sees `spawn(file, args, { shell: true })` and emits DEP0190.
The launcher keeps ordinary `.exe`/`.com` and non-Windows paths on direct argv spawning, while Windows command scripts now run through `cmd.exe /d /s /c` with `shell: false` and `windowsVerbatimArguments: true`.
Local and CI verification passed, including focused UI runner tests, build, check, Real behavior proof, and ClawSweeper gates.
Co-authored-by: Nandana Dileep <nandanadileep@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
This commit is contained in:
@@ -662,6 +662,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/reasoning: keep embedded reasoning deltas raw for correct same-line streaming while preserving formatted Telegram, Feishu, Discord, and heartbeat delivery at the channel edge. (#78397) Thanks @medns.
|
||||
- Agents/failover: rotate auth profiles before deferred cooldown marking on rate-limit failures, so file-lock contention cannot stall profile failover. Fixes #57281. (#57283) Thanks @jeremyknows.
|
||||
- Gateway/sessions: when `session.dmScope: "main"` is configured, route a bare webchat `/new` against the agent's main session (`sessions.create` with `emitCommandHooks=true`) to an in-place reset instead of creating a parallel `dashboard:` child, matching `/new` behavior on Telegram/Discord. Fixes #77434. (#71170) Thanks @statxc.
|
||||
- Scripts/UI/Windows: launch `.cmd` and `.bat` UI runners through the shared cmd.exe escaping path with shell mode disabled, avoiding Node.js v24 DEP0190 warnings while preserving argument boundaries. (#62910) Thanks @nandanadileep.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, "..");
|
||||
const uiDir = path.join(repoRoot, "ui");
|
||||
|
||||
const WINDOWS_SHELL_EXTENSIONS = new Set([".cmd", ".bat", ".com"]);
|
||||
const WINDOWS_UNSAFE_SHELL_ARG_PATTERN = /[\r\n"&|<>^%!]/;
|
||||
const WINDOWS_CMD_EXE_EXTENSIONS = new Set([".cmd", ".bat"]);
|
||||
|
||||
function usage() {
|
||||
// keep this tiny; it's invoked from npm scripts too
|
||||
@@ -53,50 +53,47 @@ function resolveRunner() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldUseShellForCommand(cmd, platform = process.platform) {
|
||||
export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
|
||||
if (platform !== "win32") {
|
||||
return false;
|
||||
}
|
||||
const extension = path.extname(cmd).toLowerCase();
|
||||
return WINDOWS_SHELL_EXTENSIONS.has(extension);
|
||||
return WINDOWS_CMD_EXE_EXTENSIONS.has(extension);
|
||||
}
|
||||
|
||||
export function assertSafeWindowsShellArgs(args, platform = process.platform) {
|
||||
if (platform !== "win32") {
|
||||
return;
|
||||
}
|
||||
const unsafeArg = args.find((arg) => WINDOWS_UNSAFE_SHELL_ARG_PATTERN.test(arg));
|
||||
if (!unsafeArg) {
|
||||
return;
|
||||
}
|
||||
// SECURITY: `shell: true` routes through cmd.exe; reject risky metacharacters
|
||||
// in forwarded args to prevent shell control-flow/env-expansion injection.
|
||||
throw new Error(
|
||||
`Unsafe Windows shell argument: ${unsafeArg}. Remove shell metacharacters (" & | < > ^ % !).`,
|
||||
);
|
||||
}
|
||||
|
||||
export function prepareSpawnCommand(cmd, platform = process.platform) {
|
||||
return shouldUseShellForCommand(cmd, platform) ? `"${cmd}"` : cmd;
|
||||
}
|
||||
|
||||
function createSpawnOptions(cmd, args, envOverride) {
|
||||
const useShell = shouldUseShellForCommand(cmd);
|
||||
if (useShell) {
|
||||
assertSafeWindowsShellArgs(args);
|
||||
}
|
||||
return {
|
||||
cwd: uiDir,
|
||||
export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
|
||||
const options = {
|
||||
cwd: params.cwd ?? uiDir,
|
||||
stdio: "inherit",
|
||||
env: envOverride ?? process.env,
|
||||
...(useShell ? { shell: true } : {}),
|
||||
shell: false,
|
||||
};
|
||||
|
||||
if (shouldUseCmdExeForCommand(cmd, platform)) {
|
||||
return {
|
||||
command: comSpec,
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine(cmd, args)],
|
||||
options: {
|
||||
...options,
|
||||
windowsVerbatimArguments: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: cmd,
|
||||
args,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args);
|
||||
let child;
|
||||
try {
|
||||
child = spawn(prepareSpawnCommand(cmd), args, createSpawnOptions(cmd, args));
|
||||
child = spawn(command, spawnArgs, options);
|
||||
} catch (err) {
|
||||
console.error(`Failed to launch ${cmd}:`, err);
|
||||
process.exit(1);
|
||||
@@ -115,9 +112,10 @@ function run(cmd, args) {
|
||||
}
|
||||
|
||||
function runSync(cmd, args, envOverride) {
|
||||
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args, envOverride);
|
||||
let result;
|
||||
try {
|
||||
result = spawnSync(prepareSpawnCommand(cmd), args, createSpawnOptions(cmd, args, envOverride));
|
||||
result = spawnSync(command, spawnArgs, options);
|
||||
} catch (err) {
|
||||
console.error(`Failed to launch ${cmd}:`, err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,49 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertSafeWindowsShellArgs,
|
||||
prepareSpawnCommand,
|
||||
shouldUseShellForCommand,
|
||||
} from "../../scripts/ui.js";
|
||||
import { resolveSpawnCall, shouldUseCmdExeForCommand } from "../../scripts/ui.js";
|
||||
|
||||
describe("scripts/ui windows spawn behavior", () => {
|
||||
it("enables shell for Windows command launchers that require cmd.exe", () => {
|
||||
it("wraps Windows command launchers with cmd.exe without enabling shell mode", () => {
|
||||
expect(
|
||||
shouldUseShellForCommand("C:\\Users\\dev\\AppData\\Local\\pnpm\\pnpm.CMD", "win32"),
|
||||
shouldUseCmdExeForCommand("C:\\Users\\dev\\AppData\\Local\\pnpm\\pnpm.CMD", "win32"),
|
||||
).toBe(true);
|
||||
expect(shouldUseShellForCommand("C:\\tools\\pnpm.bat", "win32")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not enable shell for non-shell launchers", () => {
|
||||
expect(shouldUseShellForCommand("C:\\Program Files\\nodejs\\node.exe", "win32")).toBe(false);
|
||||
expect(shouldUseShellForCommand("/usr/local/bin/pnpm", "linux")).toBe(false);
|
||||
});
|
||||
|
||||
it("quotes Windows shell launcher paths before passing them to spawn", () => {
|
||||
expect(prepareSpawnCommand("C:\\Program Files\\nodejs\\pnpm.cmd", "win32")).toBe(
|
||||
'"C:\\Program Files\\nodejs\\pnpm.cmd"',
|
||||
);
|
||||
expect(prepareSpawnCommand("C:\\Program Files\\nodejs\\pnpm.exe", "win32")).toBe(
|
||||
"C:\\Program Files\\nodejs\\pnpm.exe",
|
||||
);
|
||||
expect(prepareSpawnCommand("/usr/local/bin/pnpm", "linux")).toBe("/usr/local/bin/pnpm");
|
||||
});
|
||||
|
||||
it("allows safe forwarded args when shell mode is required on Windows", () => {
|
||||
expect(
|
||||
assertSafeWindowsShellArgs(["run", "build", "--filter", "@openclaw/ui"], "win32"),
|
||||
).toBeUndefined();
|
||||
resolveSpawnCall(
|
||||
"C:\\Program Files\\nodejs\\pnpm.cmd",
|
||||
["run", "build", "-t", "path with spaces"],
|
||||
{ PATH: "C:\\bin" },
|
||||
{ comSpec: "C:\\Windows\\System32\\cmd.exe", cwd: "C:\\repo\\ui", platform: "win32" },
|
||||
),
|
||||
).toEqual({
|
||||
command: "C:\\Windows\\System32\\cmd.exe",
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
'"C:\\Program Files\\nodejs\\pnpm.cmd" run build -t "path with spaces"',
|
||||
],
|
||||
options: {
|
||||
cwd: "C:\\repo\\ui",
|
||||
stdio: "inherit",
|
||||
env: { PATH: "C:\\bin" },
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects dangerous forwarded args when shell mode is required on Windows", () => {
|
||||
expect(() => assertSafeWindowsShellArgs(["run", "build", "evil&calc"], "win32")).toThrow(
|
||||
/unsafe windows shell argument/i,
|
||||
);
|
||||
expect(() => assertSafeWindowsShellArgs(["run", "build", "%PATH%"], "win32")).toThrow(
|
||||
/unsafe windows shell argument/i,
|
||||
);
|
||||
it("does not use cmd.exe for non-command launchers", () => {
|
||||
expect(shouldUseCmdExeForCommand("C:\\Program Files\\nodejs\\node.exe", "win32")).toBe(false);
|
||||
expect(shouldUseCmdExeForCommand("C:\\tools\\pnpm.com", "win32")).toBe(false);
|
||||
expect(shouldUseCmdExeForCommand("/usr/local/bin/pnpm", "linux")).toBe(false);
|
||||
|
||||
expect(
|
||||
resolveSpawnCall(
|
||||
"C:\\Program Files\\nodejs\\pnpm.exe",
|
||||
["run", "build"],
|
||||
{ PATH: "C:\\bin" },
|
||||
{ cwd: "C:\\repo\\ui", platform: "win32" },
|
||||
),
|
||||
).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\pnpm.exe",
|
||||
args: ["run", "build"],
|
||||
options: {
|
||||
cwd: "C:\\repo\\ui",
|
||||
stdio: "inherit",
|
||||
env: { PATH: "C:\\bin" },
|
||||
shell: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reject args on non-windows platforms", () => {
|
||||
expect(assertSafeWindowsShellArgs(["contains&metacharacters"], "linux")).toBeUndefined();
|
||||
it("rejects unsafe cmd.exe arguments before launch", () => {
|
||||
expect(() =>
|
||||
resolveSpawnCall("C:\\tools\\pnpm.cmd", ["run", "build", "evil&calc"], undefined, {
|
||||
platform: "win32",
|
||||
}),
|
||||
).toThrow(/unsafe windows cmd\.exe argument/i);
|
||||
expect(() =>
|
||||
resolveSpawnCall("C:\\tools\\pnpm.cmd", ["run", "build", "%PATH%"], undefined, {
|
||||
platform: "win32",
|
||||
}),
|
||||
).toThrow(/unsafe windows cmd\.exe argument/i);
|
||||
});
|
||||
|
||||
it("keeps non-Windows launches direct even with shell metacharacters", () => {
|
||||
expect(
|
||||
resolveSpawnCall(
|
||||
"/usr/local/bin/pnpm",
|
||||
["run", "build", "contains&metacharacters"],
|
||||
{ PATH: "/bin" },
|
||||
{ cwd: "/repo/ui", platform: "linux" },
|
||||
),
|
||||
).toEqual({
|
||||
command: "/usr/local/bin/pnpm",
|
||||
args: ["run", "build", "contains&metacharacters"],
|
||||
options: {
|
||||
cwd: "/repo/ui",
|
||||
stdio: "inherit",
|
||||
env: { PATH: "/bin" },
|
||||
shell: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user