Skip to content

Commit 88125ff

Browse files
committed
Merge branch 'ml/replace-auto-execok'
This addresses CVE-2025-46334, Git GUI malicious command injection on Windows. A malicious repository can ship versions of sh.exe or typical textconv filter programs such as astextplain. Due to the unfortunate design of Tcl on Windows, the search path when looking for an executable always includes the current directory. The mentioned programs are invoked when the user selects "Git Bash" or "Browse Files" from the menu. * ml/replace-auto-execok: git-gui: override exec and open only on Windows git-gui: sanitize $PATH on all platforms git-gui: assure PATH has only absolute elements. git-gui: cleanup git-bash menu item git-gui: avoid auto_execok in do_windows_shortcut git-gui: avoid auto_execok for git-bash menu item git-gui: remove unused proc is_shellscript git-gui: remove special treatment of Windows from open_cmd_pipe git-gui: use only the configured shell git-gui: make _shellpath usable on startup git-gui: use [is_Windows], not bad _shellpath git-gui: _which, only add .exe suffix if not present Signed-off-by: Johannes Sixt <[email protected]>
2 parents e8dd723 + a1ccd25 commit 88125ff

File tree

4 files changed

+141
-109
lines changed

4 files changed

+141
-109
lines changed

git-gui.sh

Lines changed: 136 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -77,97 +77,126 @@ proc is_Cygwin {} {
7777

7878
######################################################################
7979
##
80-
## PATH lookup
80+
## PATH lookup. Sanitize $PATH, assure exec/open use only that
81+
82+
if {[is_Windows]} {
83+
set _path_sep {;}
84+
set _search_exe .exe
85+
} else {
86+
set _path_sep {:}
87+
set _search_exe {}
88+
}
89+
90+
if {[is_Windows]} {
91+
set gitguidir [file dirname [info script]]
92+
regsub -all ";" $gitguidir "\\;" gitguidir
93+
set env(PATH) "$gitguidir;$env(PATH)"
94+
}
8195
8296
set _search_path {}
83-
proc _which {what args} {
84-
global env _search_exe _search_path
97+
set _path_seen [dict create]
98+
foreach p [split $env(PATH) $_path_sep] {
99+
# Keep only absolute paths, getting rid of ., empty, etc.
100+
if {[file pathtype $p] ne {absolute}} {
101+
continue
102+
}
103+
# Keep only the first occurence of any duplicates.
104+
set norm_p [file normalize $p]
105+
if {[dict exists $_path_seen $norm_p]} {
106+
continue
107+
}
108+
dict set _path_seen $norm_p 1
109+
lappend _search_path $norm_p
110+
}
111+
unset _path_seen
85112
86-
if {$_search_path eq {}} {
87-
if {[is_Windows]} {
88-
set gitguidir [file dirname [info script]]
89-
regsub -all ";" $gitguidir "\\;" gitguidir
90-
set env(PATH) "$gitguidir;$env(PATH)"
91-
set _search_path [split $env(PATH) {;}]
92-
# Skip empty `PATH` elements
93-
set _search_path [lsearch -all -inline -not -exact \
94-
$_search_path ""]
95-
set _search_exe .exe
113+
set env(PATH) [join $_search_path $_path_sep]
114+
115+
if {[is_Windows]} {
116+
proc _which {what args} {
117+
global _search_exe _search_path
118+
119+
if {[lsearch -exact $args -script] >= 0} {
120+
set suffix {}
121+
} elseif {[string match *$_search_exe [string tolower $what]]} {
122+
# The search string already has the file extension
123+
set suffix {}
96124
} else {
97-
set _search_path [split $env(PATH) :]
98-
set _search_exe {}
125+
set suffix $_search_exe
99126
}
100-
}
101-
102-
if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
103-
set suffix {}
104-
} else {
105-
set suffix $_search_exe
106-
}
107127
108-
foreach p $_search_path {
109-
set p [file join $p $what$suffix]
110-
if {[file exists $p]} {
111-
return [file normalize $p]
128+
foreach p $_search_path {
129+
set p [file join $p $what$suffix]
130+
if {[file exists $p]} {
131+
return [file normalize $p]
132+
}
112133
}
134+
return {}
113135
}
114-
return {}
115-
}
116136
117-
proc sanitize_command_line {command_line from_index} {
118-
set i $from_index
119-
while {$i < [llength $command_line]} {
120-
set cmd [lindex $command_line $i]
121-
if {[llength [file split $cmd]] < 2} {
122-
set fullpath [_which $cmd]
123-
if {$fullpath eq ""} {
124-
throw {NOT-FOUND} "$cmd not found in PATH"
137+
proc sanitize_command_line {command_line from_index} {
138+
set i $from_index
139+
while {$i < [llength $command_line]} {
140+
set cmd [lindex $command_line $i]
141+
if {[llength [file split $cmd]] < 2} {
142+
set fullpath [_which $cmd]
143+
if {$fullpath eq ""} {
144+
throw {NOT-FOUND} "$cmd not found in PATH"
145+
}
146+
lset command_line $i $fullpath
147+
}
148+
149+
# handle piped commands, e.g. `exec A | B`
150+
for {incr i} {$i < [llength $command_line]} {incr i} {
151+
if {[lindex $command_line $i] eq "|"} {
152+
incr i
153+
break
154+
}
125155
}
126-
lset command_line $i $fullpath
127156
}
157+
return $command_line
158+
}
159+
160+
# Override `exec` to avoid unsafe PATH lookup
128161
129-
# handle piped commands, e.g. `exec A | B`
130-
for {incr i} {$i < [llength $command_line]} {incr i} {
131-
if {[lindex $command_line $i] eq "|"} {
162+
rename exec real_exec
163+
164+
proc exec {args} {
165+
# skip options
166+
for {set i 0} {$i < [llength $args]} {incr i} {
167+
set arg [lindex $args $i]
168+
if {$arg eq "--"} {
132169
incr i
133170
break
134171
}
172+
if {[string range $arg 0 0] ne "-"} {
173+
break
174+
}
135175
}
176+
set args [sanitize_command_line $args $i]
177+
uplevel 1 real_exec $args
136178
}
137-
return $command_line
138-
}
139179
140-
# Override `exec` to avoid unsafe PATH lookup
180+
# Override `open` to avoid unsafe PATH lookup
141181
142-
rename exec real_exec
182+
rename open real_open
143183
144-
proc exec {args} {
145-
# skip options
146-
for {set i 0} {$i < [llength $args]} {incr i} {
147-
set arg [lindex $args $i]
148-
if {$arg eq "--"} {
149-
incr i
150-
break
151-
}
152-
if {[string range $arg 0 0] ne "-"} {
153-
break
184+
proc open {args} {
185+
set arg0 [lindex $args 0]
186+
if {[string range $arg0 0 0] eq "|"} {
187+
set command_line [string trim [string range $arg0 1 end]]
188+
lset args 0 "| [sanitize_command_line $command_line 0]"
154189
}
190+
uplevel 1 real_open $args
155191
}
156-
set args [sanitize_command_line $args $i]
157-
uplevel 1 real_exec $args
158-
}
159192
160-
# Override `open` to avoid unsafe PATH lookup
161-
162-
rename open real_open
193+
} else {
194+
# On non-Windows platforms, auto_execok, exec, and open are safe, and will
195+
# use the sanitized search path. But, we need _which for these.
163196
164-
proc open {args} {
165-
set arg0 [lindex $args 0]
166-
if {[string range $arg0 0 0] eq "|"} {
167-
set command_line [string trim [string range $arg0 1 end]]
168-
lset args 0 "| [sanitize_command_line $command_line 0]"
197+
proc _which {what args} {
198+
return [lindex [auto_execok $what] 0]
169199
}
170-
uplevel 1 real_open $args
171200
}
172201
173202
######################################################################
@@ -304,15 +333,37 @@ if {$_trace >= 0} {
304333
# branches).
305334
set _last_merged_branch {}
306335
307-
proc shellpath {} {
308-
global _shellpath env
309-
if {[string match @@* $_shellpath]} {
310-
if {[info exists env(SHELL)]} {
311-
return $env(SHELL)
312-
} else {
313-
return /bin/sh
314-
}
336+
# for testing, allow unconfigured _shellpath
337+
if {[string match @@* $_shellpath]} {
338+
if {[info exists env(SHELL)]} {
339+
set _shellpath $env(SHELL)
340+
} else {
341+
set _shellpath /bin/sh
315342
}
343+
}
344+
345+
if {[is_Windows]} {
346+
set _shellpath [exec cygpath -m $_shellpath]
347+
}
348+
349+
if {![file executable $_shellpath] || \
350+
!([file pathtype $_shellpath] eq {absolute})} {
351+
set errmsg "The defined shell ('$_shellpath') is not usable, \
352+
it must be an absolute path to an executable."
353+
puts stderr $errmsg
354+
355+
catch {wm withdraw .}
356+
tk_messageBox \
357+
-icon error \
358+
-type ok \
359+
-title "git-gui: configuration error" \
360+
-message $errmsg
361+
exit 1
362+
}
363+
364+
365+
proc shellpath {} {
366+
global _shellpath
316367
return $_shellpath
317368
}
318369
@@ -524,32 +575,13 @@ proc _git_cmd {name} {
524575
return $v
525576
}
526577

527-
# Test a file for a hashbang to identify executable scripts on Windows.
528-
proc is_shellscript {filename} {
529-
if {![file exists $filename]} {return 0}
530-
set f [open $filename r]
531-
fconfigure $f -encoding binary
532-
set magic [read $f 2]
533-
close $f
534-
return [expr {$magic eq "#!"}]
535-
}
536-
537-
# Run a command connected via pipes on stdout.
578+
# Run a shell command connected via pipes on stdout.
538579
# This is for use with textconv filters and uses sh -c "..." to allow it to
539-
# contain a command with arguments. On windows we must check for shell
540-
# scripts specifically otherwise just call the filter command.
580+
# contain a command with arguments. We presume this
581+
# to be a shellscript that the configured shell (/bin/sh by default) knows
582+
# how to run.
541583
proc open_cmd_pipe {cmd path} {
542-
global env
543-
if {![file executable [shellpath]]} {
544-
set exe [auto_execok [lindex $cmd 0]]
545-
if {[is_shellscript [lindex $exe 0]]} {
546-
set run [linsert [auto_execok sh] end -c "$cmd \"\$0\"" $path]
547-
} else {
548-
set run [concat $exe [lrange $cmd 1 end] $path]
549-
}
550-
} else {
551-
set run [list [shellpath] -c "$cmd \"\$0\"" $path]
552-
}
584+
set run [list [shellpath] -c "$cmd \"\$0\"" $path]
553585
return [open |$run r]
554586
}
555587

@@ -2760,17 +2792,16 @@ if {![is_bare]} {
27602792

27612793
if {[is_Windows]} {
27622794
# Use /git-bash.exe if available
2763-
set normalized [file normalize $::argv0]
2764-
regsub "/mingw../libexec/git-core/git-gui$" \
2765-
$normalized "/git-bash.exe" cmdLine
2766-
if {$cmdLine != $normalized && [file exists $cmdLine]} {
2767-
set cmdLine [list "Git Bash" $cmdLine &]
2795+
set _git_bash [exec cygpath -m /git-bash.exe]
2796+
if {[file executable $_git_bash]} {
2797+
set _bash_cmdline [list "Git Bash" $_git_bash &]
27682798
} else {
2769-
set cmdLine [list "Git Bash" bash --login -l &]
2799+
set _bash_cmdline [list "Git Bash" bash --login -l &]
27702800
}
27712801
.mbar.repository add command \
27722802
-label [mc "Git Bash"] \
2773-
-command {eval exec [auto_execok start] $cmdLine}
2803+
-command {eval exec [list [_which cmd] /c start] $_bash_cmdline}
2804+
unset _git_bash
27742805
}
27752806

27762807
if {[is_Windows] || ![is_bare]} {

lib/shortcut.tcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ proc do_windows_shortcut {} {
1212
set fn ${fn}.lnk
1313
}
1414
# Use git-gui.exe if available (ie: git-for-windows)
15-
set cmdLine [auto_execok git-gui.exe]
15+
set cmdLine [list [_which git-gui]]
1616
if {$cmdLine eq {}} {
1717
set cmdLine [list [info nameofexecutable] \
1818
[file normalize $::argv0]]

lib/sshkey.tcl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ proc make_ssh_key {w} {
8383
set sshkey_title [mc "Generating..."]
8484
$w.header.gen configure -state disabled
8585

86-
set cmdline [list sh -c {echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}]
86+
set cmdline [list [shellpath] -c \
87+
{echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}]
8788

8889
if {[catch { set sshkey_fd [_open_stdout_stderr $cmdline] } err]} {
8990
error_popup [mc "Could not start ssh-keygen:\n\n%s" $err]

lib/tools.tcl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ proc tools_exec {fullname} {
110110

111111
set cmdline $repo_config(guitool.$fullname.cmd)
112112
if {[is_config_true "guitool.$fullname.noconsole"]} {
113-
tools_run_silent [list sh -c $cmdline] \
113+
tools_run_silent [list [shellpath] -c $cmdline] \
114114
[list tools_complete $fullname {}]
115115
} else {
116116
regsub {/} $fullname { / } title
117117
set w [console::new \
118118
[mc "Tool: %s" $title] \
119119
[mc "Running: %s" $cmdline]]
120-
console::exec $w [list sh -c $cmdline] \
120+
console::exec $w [list [shellpath] -c $cmdline] \
121121
[list tools_complete $fullname $w]
122122
}
123123

0 commit comments

Comments
 (0)