| 1 | const std = @import("std"); |
| 2 | const Io = std.Io; |
| 3 | const Allocator = std.mem.Allocator; |
| 4 | |
| 5 | pub fn main(init: std.process.Init) !void { |
| 6 | const gpa = init.gpa; |
| 7 | const io = init.io; |
| 8 | |
| 9 | // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs |
| 10 | try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\x00"}); |
| 11 | try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\n"}); |
| 12 | try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\r"}); |
| 13 | try testExec(gpa, io, &.{ "a", "b" }, null); |
| 14 | try testExec(gpa, io, &.{ "c is for cat", "d is for dog" }, null); |
| 15 | try testExec(gpa, io, &.{ "\"", " \"" }, null); |
| 16 | try testExec(gpa, io, &.{ "\\", "\\" }, null); |
| 17 | try testExec(gpa, io, &.{">file.txt"}, null); |
| 18 | try testExec(gpa, io, &.{"whoami.exe"}, null); |
| 19 | try testExec(gpa, io, &.{"&a.exe"}, null); |
| 20 | try testExec(gpa, io, &.{"&echo hello "}, null); |
| 21 | try testExec(gpa, io, &.{ "&echo hello", "&whoami", ">file.txt" }, null); |
| 22 | try testExec(gpa, io, &.{"!TMP!"}, null); |
| 23 | try testExec(gpa, io, &.{"key=value"}, null); |
| 24 | try testExec(gpa, io, &.{"\"key=value\""}, null); |
| 25 | try testExec(gpa, io, &.{"key = value"}, null); |
| 26 | try testExec(gpa, io, &.{"key=[\"value\"]"}, null); |
| 27 | try testExec(gpa, io, &.{ "", "a=b" }, null); |
| 28 | try testExec(gpa, io, &.{"key=\"foo bar\""}, null); |
| 29 | try testExec(gpa, io, &.{"key=[\"my_value]"}, null); |
| 30 | try testExec(gpa, io, &.{"key=[\"my_value\",\"other-value\"]"}, null); |
| 31 | try testExec(gpa, io, &.{"key\\=value"}, null); |
| 32 | try testExec(gpa, io, &.{"key=\"&whoami\""}, null); |
| 33 | try testExec(gpa, io, &.{"key=\"value\"=5"}, null); |
| 34 | try testExec(gpa, io, &.{"key=[\">file.txt\"]"}, null); |
| 35 | try testExec(gpa, io, &.{"%hello"}, null); |
| 36 | try testExec(gpa, io, &.{"%PATH%"}, null); |
| 37 | try testExec(gpa, io, &.{"%%cd:~,%"}, null); |
| 38 | try testExec(gpa, io, &.{"%PATH%PATH%"}, null); |
| 39 | try testExec(gpa, io, &.{"\">file.txt"}, null); |
| 40 | try testExec(gpa, io, &.{"abc\"&echo hello"}, null); |
| 41 | try testExec(gpa, io, &.{"123\">file.txt"}, null); |
| 42 | try testExec(gpa, io, &.{"\"&echo hello&whoami.exe"}, null); |
| 43 | try testExec(gpa, io, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null); |
| 44 | try testExec(gpa, io, &.{"&whoami.exe"}, null); |
| 45 | |
| 46 | // Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution. |
| 47 | // In many Windows APIs (including CreateProcess), trailing space and . characters are stripped |
| 48 | // from paths, so if a path with trailing . and space character(s) is passed directly to |
| 49 | // CreateProcess, then it could end up executing a batch/cmd script that naive extension detection |
| 50 | // would not flag as .bat/.cmd. |
| 51 | // |
| 52 | // Note that we expect an error here, though, which *is* a valid mitigation, but also an implementation detail. |
| 53 | // This error is caused by the use of a wildcard with NtQueryDirectoryFile to optimize PATHEXT searching. That is, |
| 54 | // the trailing characters in the app name will lead to a FileNotFound error as the wildcard-appended path will not |
| 55 | // match any real paths on the filesystem (e.g. `foo.bat .. *` will not match `foo.bat`; only `foo.bat*` will). |
| 56 | // |
| 57 | // This being an error matches the behavior of running a command via the command line of cmd.exe, too: |
| 58 | // |
| 59 | // > "args1.bat .. " |
| 60 | // '"args1.bat .. "' is not recognized as an internal or external command, |
| 61 | // operable program or batch file. |
| 62 | try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, "args1.bat .. ", &.{"abc"}, null)); |
| 63 | const absolute_with_trailing = blk: { |
| 64 | const absolute_path = try Io.Dir.cwd().realPathFileAlloc(io, "args1.bat", gpa); |
| 65 | defer gpa.free(absolute_path); |
| 66 | break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " }); |
| 67 | }; |
| 68 | defer gpa.free(absolute_with_trailing); |
| 69 | try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, absolute_with_trailing, &.{"abc"}, null)); |
| 70 | |
| 71 | var env = env: { |
| 72 | var env = try init.environ_map.clone(gpa); |
| 73 | errdefer env.deinit(); |
| 74 | // No escaping |
| 75 | try env.put("FOO", "123"); |
| 76 | // Some possible escaping of %FOO% that could be expanded |
| 77 | // when escaping cmd.exe meta characters with ^ |
| 78 | try env.put("FOO^", "123"); // only escaping % |
| 79 | try env.put("^F^O^O^", "123"); // escaping every char |
| 80 | break :env env; |
| 81 | }; |
| 82 | defer env.deinit(); |
| 83 | try testExec(gpa, io, &.{"%FOO%"}, &env); |
| 84 | |
| 85 | // Ensure that none of the `>file.txt`s have caused file.txt to be created |
| 86 | try std.testing.expectError(error.FileNotFound, Io.Dir.cwd().access(io, "file.txt", .{})); |
| 87 | } |
| 88 | |
| 89 | fn testExecError(err: anyerror, gpa: Allocator, io: Io, args: []const []const u8) !void { |
| 90 | return std.testing.expectError(err, testExec(gpa, io, args, null)); |
| 91 | } |
| 92 | |
| 93 | fn testExec(gpa: Allocator, io: Io, args: []const []const u8, env: ?*std.process.Environ.Map) !void { |
| 94 | try testExecBat(gpa, io, "args1.bat", args, env); |
| 95 | try testExecBat(gpa, io, "args2.bat", args, env); |
| 96 | try testExecBat(gpa, io, "args3.bat", args, env); |
| 97 | } |
| 98 | |
| 99 | fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8, env: ?*std.process.Environ.Map) !void { |
| 100 | const argv = try gpa.alloc([]const u8, 1 + args.len); |
| 101 | defer gpa.free(argv); |
| 102 | argv[0] = bat; |
| 103 | @memcpy(argv[1..], args); |
| 104 | |
| 105 | const can_have_trailing_empty_args = std.mem.eql(u8, bat, "args3.bat"); |
| 106 | |
| 107 | const result = try std.process.run(gpa, io, .{ |
| 108 | .environ_map = env, |
| 109 | .argv = argv, |
| 110 | }); |
| 111 | defer gpa.free(result.stdout); |
| 112 | defer gpa.free(result.stderr); |
| 113 | |
| 114 | try std.testing.expectEqualStrings("", result.stderr); |
| 115 | var it = std.mem.splitScalar(u8, result.stdout, '\x00'); |
| 116 | var i: usize = 0; |
| 117 | while (it.next()) |actual_arg| { |
| 118 | if (i >= args.len and can_have_trailing_empty_args) { |
| 119 | try std.testing.expectEqualStrings("", actual_arg); |
| 120 | continue; |
| 121 | } |
| 122 | const expected_arg = args[i]; |
| 123 | try std.testing.expectEqualStrings(expected_arg, actual_arg); |
| 124 | i += 1; |
| 125 | } |
| 126 | } |