@@ -1095,7 +1095,7 @@ fn windowsCreateProcessPathExt(
1095
1095
}
1096
1096
};
1097
1097
const cmd_line_w = if (is_bat_or_cmd )
1098
- try cmd_line_cache .scriptCommandLine ()
1098
+ try cmd_line_cache .scriptCommandLine (full_app_name )
1099
1099
else
1100
1100
try cmd_line_cache .commandLine ();
1101
1101
const app_name_w = if (is_bat_or_cmd )
@@ -1150,7 +1150,7 @@ fn windowsCreateProcessPathExt(
1150
1150
else = > false ,
1151
1151
};
1152
1152
const cmd_line_w = if (is_bat_or_cmd )
1153
- try cmd_line_cache .scriptCommandLine ()
1153
+ try cmd_line_cache .scriptCommandLine (full_app_name )
1154
1154
else
1155
1155
try cmd_line_cache .commandLine ();
1156
1156
const app_name_w = if (is_bat_or_cmd )
@@ -1318,10 +1318,17 @@ pub const WindowsCommandLineCache = struct {
1318
1318
return self .cmd_line .? ;
1319
1319
}
1320
1320
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
+ );
1325
1332
return self .script_cmd_line .? ;
1326
1333
}
1327
1334
@@ -1375,12 +1382,15 @@ pub const ArgvToScriptCommandLineError = error{
1375
1382
/// Serializes `argv` to a Windows command-line string that uses `cmd.exe /c` and `cmd.exe`-specific
1376
1383
/// escaping rules. The caller owns the returned slice.
1377
1384
///
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:
1380
1386
/// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1381
1387
pub fn argvToScriptCommandLineWindows (
1382
1388
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 ,
1384
1394
) ArgvToScriptCommandLineError ! [:0 ]u16 {
1385
1395
var buf = try std .ArrayList (u8 ).initCapacity (allocator , 64 );
1386
1396
defer buf .deinit ();
@@ -1394,7 +1404,25 @@ pub fn argvToScriptCommandLineWindows(
1394
1404
// https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
1395
1405
buf .appendSliceAssumeCapacity ("cmd.exe /d /e:ON /v:OFF /c \" " );
1396
1406
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 | {
1398
1426
// Literal carriage returns get stripped when run through cmd.exe
1399
1427
// and NUL/newlines act as 'end of command.' Because of this, it's basically
1400
1428
// always a mistake to include these characters in argv, so it's
@@ -1405,7 +1433,7 @@ pub fn argvToScriptCommandLineWindows(
1405
1433
}
1406
1434
1407
1435
// Separate args with a space.
1408
- if ( i != 0 ) try buf .append (' ' );
1436
+ try buf .append (' ' );
1409
1437
1410
1438
// Need to quote if the argument is empty (otherwise the arg would just be lost)
1411
1439
// or if the last character is a `\`, since then something like "%~2" in a .bat
0 commit comments