summaryrefslogtreecommitdiff
path: root/git-gui/git-gui.sh
diff options
context:
space:
mode:
authorJohannes Sixt <j6t@kdbg.org>2025-05-20 08:56:09 +0200
committerTaylor Blau <me@ttaylorr.com>2025-05-23 17:04:31 -0400
commit311d9ada3a7c2c49669d656a0359cc3a9ccfeeef (patch)
tree2a415d4e14344093eda9d6e2a0daa026ae564989 /git-gui/git-gui.sh
parenta7d1716fa648f6557ea9c91e0f04bae2e8738e6a (diff)
parenta437f5bc93330a70b42a230e52f3bd036ca1b1da (diff)
Merge branch 'js/fix-open-exec'
This addresses CVE-2025-46835, Git GUI can create and overwrite a user's files: When a user clones an untrusted repository and is tricked into editing a file located in a maliciously named directory in the repository, then Git GUI can create and overwrite files for which the user has write permission. Signed-off-by: Johannes Sixt <j6t@kdbg.org>
Diffstat (limited to 'git-gui/git-gui.sh')
-rwxr-xr-xgit-gui/git-gui.sh283
1 files changed, 138 insertions, 145 deletions
diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
index 634be6a4db..bf1a20e993 100755
--- a/git-gui/git-gui.sh
+++ b/git-gui/git-gui.sh
@@ -199,6 +199,56 @@ if {[is_Windows]} {
}
}
+# Wrap exec/open to sanitize arguments
+
+# unsafe arguments begin with redirections or the pipe or background operators
+proc is_arg_unsafe {arg} {
+ regexp {^([<|>&]|2>)} $arg
+}
+
+proc make_arg_safe {arg} {
+ if {[is_arg_unsafe $arg]} {
+ set arg [file join . $arg]
+ }
+ return $arg
+}
+
+proc make_arglist_safe {arglist} {
+ set res {}
+ foreach arg $arglist {
+ lappend res [make_arg_safe $arg]
+ }
+ return $res
+}
+
+# executes one command
+# no redirections or pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# calls `exec` and returns its value
+proc safe_exec {cmd} {
+ eval exec [make_arglist_safe $cmd]
+}
+
+# executes one command in the background
+# no redirections or pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# calls `exec` and returns its value
+proc safe_exec_bg {cmd} {
+ eval exec [make_arglist_safe $cmd] &
+}
+
+proc safe_open_file {filename flags} {
+ # a file name starting with "|" would attempt to run a process
+ # but such a file name must be treated as a relative path
+ # hide the "|" behind "./"
+ if {[string index $filename 0] eq "|"} {
+ set filename [file join . $filename]
+ }
+ open $filename $flags
+}
+
+# End exec/open wrappers
+
######################################################################
##
## locate our library
@@ -299,11 +349,11 @@ unset oguimsg
if {[tk windowingsystem] eq "aqua"} {
catch {
- exec osascript -e [format {
+ safe_exec [list osascript -e [format {
tell application "System Events"
set frontmost of processes whose unix id is %d to true
end tell
- } [pid]]
+ } [pid]]]
}
}
@@ -343,7 +393,7 @@ if {[string match @@* $_shellpath]} {
}
if {[is_Windows]} {
- set _shellpath [exec cygpath -m $_shellpath]
+ set _shellpath [safe_exec [list cygpath -m $_shellpath]]
}
if {![file executable $_shellpath] || \
@@ -545,7 +595,7 @@ proc _git_cmd {name} {
# Tcl on Windows doesn't know it.
#
set p [gitexec git-$name]
- set f [open $p r]
+ set f [safe_open_file $p r]
set s [gets $f]
close $f
@@ -582,6 +632,7 @@ proc _git_cmd {name} {
# how to run.
proc open_cmd_pipe {cmd path} {
set run [list [shellpath] -c "$cmd \"\$0\"" $path]
+ set run [make_arglist_safe $run]
return [open |$run r]
}
@@ -591,7 +642,7 @@ proc _lappend_nice {cmd_var} {
if {![info exists _nice]} {
set _nice [_which nice]
- if {[catch {exec $_nice git version}]} {
+ if {[catch {safe_exec [list $_nice git version]}]} {
set _nice {}
} elseif {[is_Windows] && [file dirname $_nice] ne [file dirname $::_git]} {
set _nice {}
@@ -603,7 +654,11 @@ proc _lappend_nice {cmd_var} {
}
proc git {args} {
- set fd [eval [list git_read] $args]
+ git_redir $args {}
+}
+
+proc git_redir {cmd redir} {
+ set fd [git_read $cmd $redir]
fconfigure $fd -translation binary -encoding utf-8
set result [string trimright [read $fd] "\n"]
close $fd
@@ -613,88 +668,47 @@ proc git {args} {
return $result
}
-proc _open_stdout_stderr {cmd} {
- _trace_exec $cmd
+proc safe_open_command {cmd {redir {}}} {
+ set cmd [make_arglist_safe $cmd]
+ _trace_exec [concat $cmd $redir]
if {[catch {
- set fd [open [concat [list | ] $cmd] r]
- } err]} {
- if { [lindex $cmd end] eq {2>@1}
- && $err eq {can not find channel named "1"}
- } {
- # Older versions of Tcl 8.4 don't have this 2>@1 IO
- # redirect operator. Fallback to |& cat for those.
- # The command was not actually started, so its safe
- # to try to start it a second time.
- #
- set fd [open [concat \
- [list | ] \
- [lrange $cmd 0 end-1] \
- [list |& cat] \
- ] r]
- } else {
- error $err
- }
+ set fd [open [concat [list | ] $cmd $redir] r]
+ } err]} {
+ error $err
}
fconfigure $fd -eofchar {}
return $fd
}
-proc git_read {args} {
- set opt [list]
-
- while {1} {
- switch -- [lindex $args 0] {
- --nice {
- _lappend_nice opt
- }
+proc git_read {cmd {redir {}}} {
+ set cmdp [_git_cmd [lindex $cmd 0]]
+ set cmd [lrange $cmd 1 end]
- --stderr {
- lappend args 2>@1
- }
-
- default {
- break
- }
-
- }
-
- set args [lrange $args 1 end]
- }
-
- set cmdp [_git_cmd [lindex $args 0]]
- set args [lrange $args 1 end]
-
- return [_open_stdout_stderr [concat $opt $cmdp $args]]
+ return [safe_open_command [concat $cmdp $cmd] $redir]
}
-proc git_write {args} {
+proc git_read_nice {cmd} {
set opt [list]
- while {1} {
- switch -- [lindex $args 0] {
- --nice {
- _lappend_nice opt
- }
+ _lappend_nice opt
- default {
- break
- }
-
- }
+ set cmdp [_git_cmd [lindex $cmd 0]]
+ set cmd [lrange $cmd 1 end]
- set args [lrange $args 1 end]
- }
+ return [safe_open_command [concat $opt $cmdp $cmd]]
+}
- set cmdp [_git_cmd [lindex $args 0]]
- set args [lrange $args 1 end]
+proc git_write {cmd} {
+ set cmd [make_arglist_safe $cmd]
+ set cmdp [_git_cmd [lindex $cmd 0]]
+ set cmd [lrange $cmd 1 end]
- _trace_exec [concat $opt $cmdp $args]
- return [open [concat [list | ] $opt $cmdp $args] w]
+ _trace_exec [concat $cmdp $cmd]
+ return [open [concat [list | ] $cmdp $cmd] w]
}
proc githook_read {hook_name args} {
- set cmd [concat git hook run --ignore-missing $hook_name -- $args 2>@1]
- return [_open_stdout_stderr $cmd]
+ git_read [concat [list hook run --ignore-missing $hook_name --] $args] [list 2>@1]
}
proc kill_file_process {fd} {
@@ -702,9 +716,9 @@ proc kill_file_process {fd} {
catch {
if {[is_Windows]} {
- exec taskkill /pid $process
+ safe_exec [list taskkill /pid $process]
} else {
- exec kill $process
+ safe_exec [list kill $process]
}
}
}
@@ -730,7 +744,7 @@ proc sq {value} {
proc load_current_branch {} {
global current_branch is_detached
- set fd [open [gitdir HEAD] r]
+ set fd [safe_open_file [gitdir HEAD] r]
fconfigure $fd -translation binary -encoding utf-8
if {[gets $fd ref] < 1} {
set ref {}
@@ -1092,7 +1106,7 @@ You are using [git-version]:
## configure our library
set idx [file join $oguilib tclIndex]
-if {[catch {set fd [open $idx r]} err]} {
+if {[catch {set fd [safe_open_file $idx r]} err]} {
catch {wm withdraw .}
tk_messageBox \
-icon error \
@@ -1130,53 +1144,30 @@ unset -nocomplain idx fd
##
## config file parsing
-git-version proc _parse_config {arr_name args} {
- >= 1.5.3 {
- upvar $arr_name arr
- array unset arr
- set buf {}
- catch {
- set fd_rc [eval \
- [list git_read config] \
- $args \
- [list --null --list]]
- fconfigure $fd_rc -translation binary -encoding utf-8
- set buf [read $fd_rc]
- close $fd_rc
- }
- foreach line [split $buf "\0"] {
- if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
- if {[is_many_config $name]} {
- lappend arr($name) $value
- } else {
- set arr($name) $value
- }
- } elseif {[regexp {^([^\n]+)$} $line line name]} {
- # no value given, but interpreting them as
- # boolean will be handled as true
- set arr($name) {}
- }
- }
- }
- default {
- upvar $arr_name arr
- array unset arr
- catch {
- set fd_rc [eval [list git_read config --list] $args]
- while {[gets $fd_rc line] >= 0} {
- if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
- if {[is_many_config $name]} {
- lappend arr($name) $value
- } else {
- set arr($name) $value
- }
- } elseif {[regexp {^([^=]+)$} $line line name]} {
- # no value given, but interpreting them as
- # boolean will be handled as true
- set arr($name) {}
- }
+proc _parse_config {arr_name args} {
+ upvar $arr_name arr
+ array unset arr
+ set buf {}
+ catch {
+ set fd_rc [git_read \
+ [concat config \
+ $args \
+ --null --list]]
+ fconfigure $fd_rc -translation binary -encoding utf-8
+ set buf [read $fd_rc]
+ close $fd_rc
+ }
+ foreach line [split $buf "\0"] {
+ if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
+ if {[is_many_config $name]} {
+ lappend arr($name) $value
+ } else {
+ set arr($name) $value
}
- close $fd_rc
+ } elseif {[regexp {^([^\n]+)$} $line line name]} {
+ # no value given, but interpreting them as
+ # boolean will be handled as true
+ set arr($name) {}
}
}
}
@@ -1452,7 +1443,7 @@ proc repository_state {ctvar hdvar mhvar} {
set merge_head [gitdir MERGE_HEAD]
if {[file exists $merge_head]} {
set ct merge
- set fd_mh [open $merge_head r]
+ set fd_mh [safe_open_file $merge_head r]
while {[gets $fd_mh line] >= 0} {
lappend mh $line
}
@@ -1471,7 +1462,7 @@ proc PARENT {} {
return $p
}
if {$empty_tree eq {}} {
- set empty_tree [git mktree << {}]
+ set empty_tree [git_redir [list mktree] [list << {}]]
}
return $empty_tree
}
@@ -1530,12 +1521,12 @@ proc rescan {after {honor_trustmtime 1}} {
} else {
set rescan_active 1
ui_status [mc "Refreshing file status..."]
- set fd_rf [git_read update-index \
+ set fd_rf [git_read [list update-index \
-q \
--unmerged \
--ignore-missing \
--refresh \
- ]
+ ]]
fconfigure $fd_rf -blocking 0 -translation binary
fileevent $fd_rf readable \
[list rescan_stage2 $fd_rf $after]
@@ -1575,11 +1566,11 @@ proc rescan_stage2 {fd after} {
set rescan_active 2
ui_status [mc "Scanning for modified files ..."]
if {[git-version >= "1.7.2"]} {
- set fd_di [git_read diff-index --cached --ignore-submodules=dirty -z [PARENT]]
+ set fd_di [git_read [list diff-index --cached --ignore-submodules=dirty -z [PARENT]]]
} else {
- set fd_di [git_read diff-index --cached -z [PARENT]]
+ set fd_di [git_read [list diff-index --cached -z [PARENT]]]
}
- set fd_df [git_read diff-files -z]
+ set fd_df [git_read [list diff-files -z]]
fconfigure $fd_di -blocking 0 -translation binary -encoding binary
fconfigure $fd_df -blocking 0 -translation binary -encoding binary
@@ -1588,7 +1579,7 @@ proc rescan_stage2 {fd after} {
fileevent $fd_df readable [list read_diff_files $fd_df $after]
if {[is_config_true gui.displayuntracked]} {
- set fd_lo [eval git_read ls-files --others -z $ls_others]
+ set fd_lo [git_read [concat ls-files --others -z $ls_others]]
fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
incr rescan_active
@@ -1600,7 +1591,7 @@ proc load_message {file {encoding {}}} {
set f [gitdir $file]
if {[file isfile $f]} {
- if {[catch {set fd [open $f r]}]} {
+ if {[catch {set fd [safe_open_file $f r]}]} {
return 0
}
fconfigure $fd -eofchar {}
@@ -1624,23 +1615,23 @@ proc run_prepare_commit_msg_hook {} {
# it will be .git/MERGE_MSG (merge), .git/SQUASH_MSG (squash), or an
# empty file but existent file.
- set fd_pcm [open [gitdir PREPARE_COMMIT_MSG] a]
+ set fd_pcm [safe_open_file [gitdir PREPARE_COMMIT_MSG] a]
if {[file isfile [gitdir MERGE_MSG]]} {
set pcm_source "merge"
- set fd_mm [open [gitdir MERGE_MSG] r]
+ set fd_mm [safe_open_file [gitdir MERGE_MSG] r]
fconfigure $fd_mm -encoding utf-8
puts -nonewline $fd_pcm [read $fd_mm]
close $fd_mm
} elseif {[file isfile [gitdir SQUASH_MSG]]} {
set pcm_source "squash"
- set fd_sm [open [gitdir SQUASH_MSG] r]
+ set fd_sm [safe_open_file [gitdir SQUASH_MSG] r]
fconfigure $fd_sm -encoding utf-8
puts -nonewline $fd_pcm [read $fd_sm]
close $fd_sm
} elseif {[file isfile [get_config commit.template]]} {
set pcm_source "template"
- set fd_sm [open [get_config commit.template] r]
+ set fd_sm [safe_open_file [get_config commit.template] r]
fconfigure $fd_sm -encoding utf-8
puts -nonewline $fd_pcm [read $fd_sm]
close $fd_sm
@@ -2230,7 +2221,7 @@ proc do_gitk {revs {is_submodule false}} {
unset env(GIT_DIR)
unset env(GIT_WORK_TREE)
}
- eval exec $cmd $revs "--" "--" &
+ safe_exec_bg [concat $cmd $revs "--" "--"]
set env(GIT_DIR) $_gitdir
set env(GIT_WORK_TREE) $_gitworktree
@@ -2267,7 +2258,7 @@ proc do_git_gui {} {
set pwd [pwd]
cd $current_diff_path
- eval exec $exe gui &
+ safe_exec_bg [concat $exe gui]
set env(GIT_DIR) $_gitdir
set env(GIT_WORK_TREE) $_gitworktree
@@ -2298,16 +2289,18 @@ proc get_explorer {} {
proc do_explore {} {
global _gitworktree
- set explorer [get_explorer]
- eval exec $explorer [list [file nativename $_gitworktree]] &
+ set cmd [get_explorer]
+ lappend cmd [file nativename $_gitworktree]
+ safe_exec_bg $cmd
}
# Open file relative to the working tree by the default associated app.
proc do_file_open {file} {
global _gitworktree
- set explorer [get_explorer]
+ set cmd [get_explorer]
set full_file_path [file join $_gitworktree $file]
- exec $explorer [file nativename $full_file_path] &
+ lappend cmd [file nativename $full_file_path]
+ safe_exec_bg $cmd
}
set is_quitting 0
@@ -2341,7 +2334,7 @@ proc do_quit {{rc {1}}} {
if {![string match amend* $commit_type]
&& $msg ne {}} {
catch {
- set fd [open $save w]
+ set fd [safe_open_file $save w]
fconfigure $fd -encoding utf-8
puts -nonewline $fd $msg
close $fd
@@ -2785,15 +2778,15 @@ if {![is_bare]} {
if {[is_Windows]} {
# Use /git-bash.exe if available
- set _git_bash [exec cygpath -m /git-bash.exe]
+ set _git_bash [safe_exec [list cygpath -m /git-bash.exe]]
if {[file executable $_git_bash]} {
- set _bash_cmdline [list "Git Bash" $_git_bash &]
+ set _bash_cmdline [list "Git Bash" $_git_bash]
} else {
- set _bash_cmdline [list "Git Bash" bash --login -l &]
+ set _bash_cmdline [list "Git Bash" bash --login -l]
}
.mbar.repository add command \
-label [mc "Git Bash"] \
- -command {eval exec [list [_which cmd] /c start] $_bash_cmdline}
+ -command {safe_exec_bg [concat [list [_which cmd] /c start] $_bash_cmdline]}
unset _git_bash
}
@@ -4101,7 +4094,7 @@ if {[winfo exists $ui_comm]} {
}
} elseif {$m} {
catch {
- set fd [open [gitdir GITGUI_BCK] w]
+ set fd [safe_open_file [gitdir GITGUI_BCK] w]
fconfigure $fd -encoding utf-8
puts -nonewline $fd $msg
close $fd