Skip to content

Commit bf1132b

Browse files
committed
fix(compat): reject quote-bearing args in cmd shims, drop backslash-quote escaping
cmd.exe does not treat backslash as a quote escape: the '"' in an emitted '\"' toggles the quoted region, exposing any following metacharacter as command syntax (verified: 'a"&echo x' chained the second command). No escaping survives both layers: - '\"' — cmd layer injection, as reviewed. - '""' doubling — safe at the cmd layer, but ambiguous to the child: node's argv parser reads '""' as a literal quote while bun's splits the argument in two (verified), so the sender cannot guarantee semantics for arbitrary targets. Arguments containing '"' now throw like '%' and control characters. Node.js reached the same conclusion more broadly: since the CVE-2024-27980 hardening it refuses to spawn .cmd/.bat files with any arguments at all (EINVAL). The retained safe subset (no quotes, no %, no control chars) covers every current call site; trailing-backslash doubling for quoted arguments is kept and verified against both node and bun children.
1 parent 47bb347 commit bf1132b

2 files changed

Lines changed: 39 additions & 17 deletions

File tree

src/utils/compat.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,6 @@ describe('buildWindowsCommandLine', () => {
212212
);
213213
});
214214

215-
it('escapes embedded quotes in quoted arguments', () => {
216-
expect(buildWindowsCommandLine('bun', ['a"b'])).toBe('"bun "a\\"b""');
217-
});
218-
219215
it('quotes cmd metacharacters so cmd.exe treats them literally', () => {
220216
// Unquoted, `&` would let cmd chain a second command — the argument
221217
// must end up inside double quotes on the final command line.
@@ -232,6 +228,14 @@ describe('buildWindowsCommandLine', () => {
232228
expect(() => buildWindowsCommandLine('bun', ['a%PATH%b'])).toThrow();
233229
});
234230

231+
it('rejects double quotes that would toggle the cmd quoted region', () => {
232+
// `\"` is not an escape for cmd.exe; a quote followed by a
233+
// metacharacter would expose command syntax (verified as injection).
234+
expect(() => buildWindowsCommandLine('bun', ['a"&b'])).toThrow(/'"'/);
235+
expect(() => buildWindowsCommandLine('bun', ['a"b'])).toThrow();
236+
expect(() => buildWindowsCommandLine('bun', ['he said "hi"'])).toThrow();
237+
});
238+
235239
it('rejects control characters that corrupt the cmd line', () => {
236240
expect(() => buildWindowsCommandLine('bun', ['a\nb'])).toThrow();
237241
expect(() => buildWindowsCommandLine('bun', ['a\rb'])).toThrow();

src/utils/compat.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,20 +159,35 @@ export function resolveWindowsCommand(
159159
* cmd.exe metacharacters neutralised by double-quoting the argument.
160160
* Inside double quotes, `& | < > ( ) ^ !` are literal to cmd; unquoted
161161
* they split or chain commands (verified on Windows: passing `a&echo x`
162-
* as a bare token makes cmd execute the second command).
162+
* as a bare token makes cmd execute the second command). `"` is not
163+
* listed — arguments containing it are rejected by
164+
* isCmdUnsafeArgument instead.
163165
*/
164-
const CMD_METACHARACTERS = /[\s"&|<>()^!]/;
166+
const CMD_METACHARACTERS = /[\s&|<>()^!]/;
165167

166168
/**
167169
* Detects characters that cannot be passed through `cmd.exe /c` at
168-
* all: `%` expands environment variables even inside double quotes and
169-
* has no escape on the cmd command line, and control characters corrupt
170-
* the line. Node.js rejects the same inputs with EINVAL when spawning
171-
* .cmd/.bat files (CVE-2024-27980 hardening); we throw instead of
172-
* letting cmd.exe reinterpret the argument.
170+
* all:
171+
*
172+
* - `%` expands environment variables even inside double quotes and
173+
* has no escape on the cmd command line.
174+
* - `"` toggles cmd's quoted region no matter what precedes it —
175+
* cmd.exe, unlike the MSVCRT argument parser, does not treat `\` as
176+
* an escape. Emitting `\"` therefore closes the quoted region and
177+
* exposes any following metacharacter as command syntax (verified:
178+
* `a"&echo x` injected the second command). Doubling quotes instead
179+
* (`a""&b`) stays safe at the cmd layer but is ambiguous to the
180+
* child: node's argv parser reads `""` as a literal quote while
181+
* bun's splits the argument in two (verified), so no single
182+
* escaping works across targets.
183+
* - Control characters corrupt the command line.
184+
*
185+
* Node.js refuses to spawn `.cmd`/`.bat` files with any arguments at
186+
* all for the same class of reasons (EINVAL since CVE-2024-27980
187+
* hardening); we keep the provably-safe subset and throw otherwise.
173188
*/
174189
function isCmdUnsafeArgument(arg: string): boolean {
175-
if (arg.includes('%')) return true;
190+
if (arg.includes('%') || arg.includes('"')) return true;
176191
for (let i = 0; i < arg.length; i++) {
177192
if (arg.charCodeAt(i) <= 0x1f) return true;
178193
}
@@ -182,22 +197,25 @@ function isCmdUnsafeArgument(arg: string): boolean {
182197
/**
183198
* Quotes one argument for a `cmd.exe /c` command line. Arguments that
184199
* contain no metacharacters are passed through untouched — cmd's /s
185-
* stripping mangles gratuitously quoted tokens.
200+
* stripping mangles gratuitously quoted tokens. Quoted arguments have
201+
* no `"` left to escape (those throw in isCmdUnsafeArgument); only a
202+
* trailing backslash run must be doubled so the closing quote is not
203+
* read as an escape by the child's argument parser.
186204
*
187205
* Throws on arguments that cmd.exe cannot represent faithfully (`%`,
188-
* control characters) so callers fail loudly instead of executing an
189-
* altered command line.
206+
* `"`, control characters) so callers fail loudly instead of
207+
* executing an altered command line.
190208
*/
191209
function escapeWindowsArgument(arg: string): string {
192210
if (isCmdUnsafeArgument(arg)) {
193211
throw new Error(
194-
`cannot pass ${JSON.stringify(arg)} through a .cmd shim: cmd.exe reinterprets '%' and control characters even inside quotes`,
212+
`cannot pass ${JSON.stringify(arg)} through a .cmd shim: cmd.exe reinterprets '%', '"', and control characters even inside quotes`,
195213
);
196214
}
197215
if (!CMD_METACHARACTERS.test(arg)) {
198216
return arg;
199217
}
200-
const escaped = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1');
218+
const escaped = arg.replace(/(\\*)$/, '$1$1');
201219
return `"${escaped}"`;
202220
}
203221

0 commit comments

Comments
 (0)