gitk(Windows): avoid inadvertently calling executables in the worktree

Just like CVE-2022-41953 for Git GUI, there exists a vulnerability of `gitk` where it looks for `taskkill.exe` in the current directory before searching `PATH`. Note that the many `exec git` calls are unaffected, due to an obscure quirk in Tcl's `exec` function. Typically, `git.exe` lives next to `wish.exe` (i.e. the program that is run to execute `gitk` or Git GUI) in Git for Windows, and that is the saving grace for `git.exe because `exec` searches the directory where `wish.exe` lives even before the current directory, according to https://www.tcl-lang.org/man/tcl/TclCmd/exec.htm#M24: If a directory name was not specified as part of the application name, the following directories are automatically searched in order when attempting to locate the application: The directory from which the Tcl executable was loaded. The current directory. The Windows 32-bit system directory. The Windows home directory. The directories listed in the path. The same is not true, however, for `taskkill.exe`: it lives in the Windows system directory (never mind the 32-bit, Tcl's documentation is outdated on that point, it really means `C:\Windows\system32`). Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

Johannes Schindelin committed Jan 19, 2023 at 13:40 UTC 4cbe9e0e21fca682d78980a03184071e9fe1c955
1 file changed +135
gitk
+135
@@ -9,6 +9,141 @@ exec wish "$0" -- "$@"
9
10 package require Tk
11
12 +######################################################################
13 +##
14 +## Enabling platform-specific code paths
15 +
16 +proc is_MacOSX {} {
17 + if {[tk windowingsystem] eq {aqua}} {
18 + return 1
19 + }
20 + return 0
21 +}
22 +
23 +proc is_Windows {} {
24 + if {$::tcl_platform(platform) eq {windows}} {
25 + return 1
26 + }
27 + return 0
28 +}
29 +
30 +set _iscygwin {}
31 +proc is_Cygwin {} {
32 + global _iscygwin
33 + if {$_iscygwin eq {}} {
34 + if {[string match "CYGWIN_*" $::tcl_platform(os)]} {
35 + set _iscygwin 1
36 + } else {
37 + set _iscygwin 0
38 + }
39 + }
40 + return $_iscygwin
41 +}
42 +
43 +######################################################################
44 +##
45 +## PATH lookup
46 +
47 +set _search_path {}
48 +proc _which {what args} {
49 + global env _search_exe _search_path
50 +
51 + if {$_search_path eq {}} {
52 + if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
53 + set _search_path [split [exec cygpath \
54 + --windows \
55 + --path \
56 + --absolute \
57 + $env(PATH)] {;}]
58 + set _search_exe .exe
59 + } elseif {[is_Windows]} {
60 + set gitguidir [file dirname [info script]]
61 + regsub -all ";" $gitguidir "\\;" gitguidir
62 + set env(PATH) "$gitguidir;$env(PATH)"
63 + set _search_path [split $env(PATH) {;}]
64 + # Skip empty `PATH` elements
65 + set _search_path [lsearch -all -inline -not -exact \
66 + $_search_path ""]
67 + set _search_exe .exe
68 + } else {
69 + set _search_path [split $env(PATH) :]
70 + set _search_exe {}
71 + }
72 + }
73 +
74 + if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
75 + set suffix {}
76 + } else {
77 + set suffix $_search_exe
78 + }
79 +
80 + foreach p $_search_path {
81 + set p [file join $p $what$suffix]
82 + if {[file exists $p]} {
83 + return [file normalize $p]
84 + }
85 + }
86 + return {}
87 +}
88 +
89 +proc sanitize_command_line {command_line from_index} {
90 + set i $from_index
91 + while {$i < [llength $command_line]} {
92 + set cmd [lindex $command_line $i]
93 + if {[file pathtype $cmd] ne "absolute"} {
94 + set fullpath [_which $cmd]
95 + if {$fullpath eq ""} {
96 + throw {NOT-FOUND} "$cmd not found in PATH"
97 + }
98 + lset command_line $i $fullpath
99 + }
100 +
101 + # handle piped commands, e.g. `exec A | B`
102 + for {incr i} {$i < [llength $command_line]} {incr i} {
103 + if {[lindex $command_line $i] eq "|"} {
104 + incr i
105 + break
106 + }
107 + }
108 + }
109 + return $command_line
110 +}
111 +
112 +# Override `exec` to avoid unsafe PATH lookup
113 +
114 +rename exec real_exec
115 +
116 +proc exec {args} {
117 + # skip options
118 + for {set i 0} {$i < [llength $args]} {incr i} {
119 + set arg [lindex $args $i]
120 + if {$arg eq "--"} {
121 + incr i
122 + break
123 + }
124 + if {[string range $arg 0 0] ne "-"} {
125 + break
126 + }
127 + }
128 + set args [sanitize_command_line $args $i]
129 + uplevel 1 real_exec $args
130 +}
131 +
132 +# Override `open` to avoid unsafe PATH lookup
133 +
134 +rename open real_open
135 +
136 +proc open {args} {
137 + set arg0 [lindex $args 0]
138 + if {[string range $arg0 0 0] eq "|"} {
139 + set command_line [string trim [string range $arg0 1 end]]
140 + lset args 0 "| [sanitize_command_line $command_line 0]"
141 + }
142 + uplevel 1 real_open $args
143 +}
144 +
145 +# End of safe PATH lookup stuff
146 +
147 proc hasworktree {} {
148 return [expr {[exec git rev-parse --is-bare-repository] == "false" &&
149 [exec git rev-parse --is-inside-git-dir] == "false"}]