Files
rea/src/process/WindowsBatchCommand.ts
T
Kaoru0822 ae95e68ba5 fix(apktool): run Windows batch launchers through cmd.exe (#1777)
On Windows, Node refuses to spawn .bat and .cmd files without a shell
(CVE-2024-27980), so REA_APKTOOL_COMMAND pointing at Chocolatey's
apktool.bat or a java -jar wrapper failed its version probe with EINVAL,
reported as "did not report a usable version".

Move Ghidra's conservative cmd.exe invocation (no AutoRun, no delayed
expansion, every token quoted, interpreter metacharacters rejected) into
src/process/WindowsBatchCommand.ts. Ghidra keeps its error type, wording
and length limit. apktool now uses it for both the version probe and the
decode run when the launcher is a batch file on Windows. A launcher that
cmd.exe would reinterpret reports launcher_unsupported (not_configured)
with remediation instead of version_unresolved.

Fixes #1774

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:16:48 +08:00

82 lines
2.5 KiB
TypeScript

import { win32 } from "node:path";
import { snapshotEnvironment } from "./snapshotEnvironment.js";
/** Why a batch launcher cannot be passed safely to cmd.exe. */
export type WindowsBatchCommandFailure = "invalid_comspec" | "metacharacter";
export class WindowsBatchCommandError extends Error {
constructor(
readonly reason: WindowsBatchCommandFailure,
message: string,
) {
super(message);
this.name = "WindowsBatchCommandError";
}
}
/** Exact executable and argv passed to shell-free process creation. */
export interface WindowsBatchCommand {
readonly command: string;
/** Pass verbatim: cmd.exe parses this line itself, not by CRT argv rules. */
readonly arguments: readonly string[];
}
/**
* Windows runs `.bat` and `.cmd` files only through cmd.exe, so Node rejects
* them without a shell (CVE-2024-27980).
*/
export const isWindowsBatchFile = (command: string): boolean =>
/\.(?:bat|cmd)$/iu.test(command);
/**
* Quote one token for cmd.exe. Tokens that cmd.exe could expand, split or
* redirect are rejected instead of escaped.
*/
export const quoteWindowsBatchToken = (value: string): string => {
if (
value.length === 0 ||
value.includes("\0") ||
value.includes("\r") ||
value.includes("\n") ||
/["%^!&|<>]/u.test(value)
)
throw new WindowsBatchCommandError(
"metacharacter",
"Windows batch arguments cannot contain command-interpreter metacharacters",
);
return `"${value}"`;
};
/** Run a batch file through an explicit, non-AutoRun cmd.exe invocation. */
export const windowsBatchCommand = (options: {
readonly environment: Readonly<NodeJS.ProcessEnv>;
readonly batchPath: string;
readonly arguments: readonly string[];
readonly comSpec?: string;
}): WindowsBatchCommand => {
const environment = snapshotEnvironment(options.environment, "win32");
const systemRoot = environment.SYSTEMROOT;
const comSpec =
options.comSpec ??
environment.COMSPEC ??
(systemRoot === undefined
? "C:\\Windows\\System32\\cmd.exe"
: win32.join(systemRoot, "System32", "cmd.exe"));
if (
!win32.isAbsolute(comSpec) ||
win32.basename(comSpec).toLowerCase() !== "cmd.exe"
)
throw new WindowsBatchCommandError(
"invalid_comspec",
"Windows ComSpec must be an absolute cmd.exe path",
);
const tokens = [options.batchPath, ...options.arguments].map(
quoteWindowsBatchToken,
);
return {
command: comSpec,
arguments: ["/d", "/e:on", "/v:off", "/s", "/c", `"${tokens.join(" ")}"`],
};
};