Skip to content

Commit a4ff99f

Browse files
committed
Use a more explicit path to the script file when spawning via cmd.exe
In order to avoid cmd.exe searching the PATH, we make sure the path to the script is as qualified as possible, and we add `.\` in front if it doesn't have any path separators (the presence of a path separator avoids PATH searching). This is just an optimization/nicety now that we're spawning cmd.exe directly instead of letting CreateProcessW spawn it for us.
1 parent 38b928f commit a4ff99f

File tree

1 file changed

+39
-11
lines changed

1 file changed

+39
-11
lines changed

lib/std/child_process.zig

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@ fn windowsCreateProcessPathExt(
10951095
}
10961096
};
10971097
const cmd_line_w = if (is_bat_or_cmd)
1098-
try cmd_line_cache.scriptCommandLine()
1098+
try cmd_line_cache.scriptCommandLine(full_app_name)
10991099
else
11001100
try cmd_line_cache.commandLine();
11011101
const app_name_w = if (is_bat_or_cmd)
@@ -1150,7 +1150,7 @@ fn windowsCreateProcessPathExt(
11501150
else => false,
11511151
};
11521152
const cmd_line_w = if (is_bat_or_cmd)
1153-
try cmd_line_cache.scriptCommandLine()
1153+
try cmd_line_cache.scriptCommandLine(full_app_name)
11541154
else
11551155
try cmd_line_cache.commandLine();
11561156
const app_name_w = if (is_bat_or_cmd)
@@ -1318,10 +1318,17 @@ pub const WindowsCommandLineCache = struct {
13181318
return self.cmd_line.?;
13191319
}
13201320

1321-
pub fn scriptCommandLine(self: *WindowsCommandLineCache) ![:0]u16 {
1322-
if (self.script_cmd_line == null) {
1323-
self.script_cmd_line = try argvToScriptCommandLineWindows(self.allocator, self.argv);
1324-
}
1321+
/// Not cached, since the path to the batch script will change during PATH searching.
1322+
/// `script_path` should be as qualified as possible, e.g. if the PATH is being searched,
1323+
/// then script_path should include both the search path and the script filename
1324+
/// (this allows avoiding cmd.exe having to search the PATH again).
1325+
pub fn scriptCommandLine(self: *WindowsCommandLineCache, script_path: []const u16) ![:0]u16 {
1326+
if (self.script_cmd_line) |v| self.allocator.free(v);
1327+
self.script_cmd_line = try argvToScriptCommandLineWindows(
1328+
self.allocator,
1329+
script_path,
1330+
self.argv[1..],
1331+
);
13251332
return self.script_cmd_line.?;
13261333
}
13271334

@@ -1375,12 +1382,15 @@ pub const ArgvToScriptCommandLineError = error{
13751382
/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
13761383
/// escaping rules. The caller owns the returned slice.
13771384
///
1378-
/// Escapes `argv` using the suggested mitigation against
1379-
/// arbitrary command execution from:
1385+
/// Escapes `argv` using the suggested mitigation against arbitrary command execution from:
13801386
/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
13811387
pub fn argvToScriptCommandLineWindows(
13821388
allocator: mem.Allocator,
1383-
argv: []const []const u8,
1389+
/// Path to the `.bat`/`.cmd` script. If this path is relative, it is assumed to be relative to the CWD.
1390+
/// The script must have been verified to exist at this path before calling this function.
1391+
script_path: []const u16,
1392+
/// Arguments, not including the script name itself. Expected to be encoded as WTF-8.
1393+
script_args: []const []const u8,
13841394
) ArgvToScriptCommandLineError![:0]u16 {
13851395
var buf = try std.ArrayList(u8).initCapacity(allocator, 64);
13861396
defer buf.deinit();
@@ -1394,7 +1404,25 @@ pub fn argvToScriptCommandLineWindows(
13941404
// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
13951405
buf.appendSliceAssumeCapacity("cmd.exe /d /e:ON /v:OFF /c \"");
13961406

1397-
for (argv, 0..) |arg, i| {
1407+
// Always quote the path to the script arg
1408+
buf.appendAssumeCapacity('"');
1409+
// We always want the path to the batch script to include a path separator in order to
1410+
// avoid cmd.exe searching the PATH for the script. This is not part of the arbitrary
1411+
// command execution mitigation, we just know exactly what script we want to execute
1412+
// at this point, and potentially making cmd.exe re-find it is unnecessary.
1413+
//
1414+
// If the script path does not have a path separator, then we know its relative to CWD and
1415+
// we can just put `.\` in the front.
1416+
if (mem.indexOfAny(u16, script_path, &[_]u16{mem.nativeToLittle(u16, '\\'), mem.nativeToLittle(u16, '/')}) == null) {
1417+
try buf.appendSlice(".\\");
1418+
}
1419+
// Note that we don't do any escaping/mitigations for this argument, since the relevant
1420+
// characters (", %, etc) are illegal in file paths and this function should only be called
1421+
// with script paths that have been verified to exist.
1422+
try std.unicode.wtf16LeToWtf8ArrayList(&buf, script_path);
1423+
buf.appendAssumeCapacity('"');
1424+
1425+
for (script_args) |arg| {
13981426
// Literal carriage returns get stripped when run through cmd.exe
13991427
// and NUL/newlines act as 'end of command.' Because of this, it's basically
14001428
// always a mistake to include these characters in argv, so it's
@@ -1405,7 +1433,7 @@ pub fn argvToScriptCommandLineWindows(
14051433
}
14061434

14071435
// Separate args with a space.
1408-
if (i != 0) try buf.append(' ');
1436+
try buf.append(' ');
14091437

14101438
// Need to quote if the argument is empty (otherwise the arg would just be lost)
14111439
// or if the last character is a `\`, since then something like "%~2" in a .bat

0 commit comments

Comments
 (0)