diff options
| author | Johannes Sixt <j6t@kdbg.org> | 2025-05-20 08:56:09 +0200 |
|---|---|---|
| committer | Taylor Blau <me@ttaylorr.com> | 2025-05-23 17:04:31 -0400 |
| commit | 311d9ada3a7c2c49669d656a0359cc3a9ccfeeef (patch) | |
| tree | 2a415d4e14344093eda9d6e2a0daa026ae564989 /git-gui/git-gui.sh | |
| parent | a7d1716fa648f6557ea9c91e0f04bae2e8738e6a (diff) | |
| parent | a437f5bc93330a70b42a230e52f3bd036ca1b1da (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-x | git-gui/git-gui.sh | 283 |
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 |
