Merge branch 'js/windows-rce'

Fix a Remote Code Execution vulnerability on Windows. This is caused by
the fact that Tcl on Windows always includes the current directory when
looking for an executable. Therefore malicious repositories can ship
with an aspell.exe in their top-level directory which is executed by Git
GUI without giving the user a chance to inspect it first, i.e. running
untrusted code.

This merge fixes CVE-2022-41953.

* js/windows-rce:
  Work around Tcl's default `PATH` lookup
  Move the `_which` function (almost) to the top
  Move is_<platform> functions to the beginning
  is_Cygwin: avoid `exec`ing anything
  windows: ignore empty `PATH` elements
maint
Pratyush Yadav 2023-01-24 14:13:46 +01:00
commit 1e5a89c1b4
1 changed files with 133 additions and 74 deletions

View File

@ -44,6 +44,139 @@ if {[catch {package require Tcl 8.5} err]

catch {rename send {}} ; # What an evil concept...

######################################################################
##
## Enabling platform-specific code paths

proc is_MacOSX {} {
if {[tk windowingsystem] eq {aqua}} {
return 1
}
return 0
}

proc is_Windows {} {
if {$::tcl_platform(platform) eq {windows}} {
return 1
}
return 0
}

set _iscygwin {}
proc is_Cygwin {} {
global _iscygwin
if {$_iscygwin eq {}} {
if {[string match "CYGWIN_*" $::tcl_platform(os)]} {
set _iscygwin 1
} else {
set _iscygwin 0
}
}
return $_iscygwin
}

######################################################################
##
## PATH lookup

set _search_path {}
proc _which {what args} {
global env _search_exe _search_path

if {$_search_path eq {}} {
if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
set _search_path [split [exec cygpath \
--windows \
--path \
--absolute \
$env(PATH)] {;}]
set _search_exe .exe
} elseif {[is_Windows]} {
set gitguidir [file dirname [info script]]
regsub -all ";" $gitguidir "\\;" gitguidir
set env(PATH) "$gitguidir;$env(PATH)"
set _search_path [split $env(PATH) {;}]
# Skip empty `PATH` elements
set _search_path [lsearch -all -inline -not -exact \
$_search_path ""]
set _search_exe .exe
} else {
set _search_path [split $env(PATH) :]
set _search_exe {}
}
}

if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
set suffix {}
} else {
set suffix $_search_exe
}

foreach p $_search_path {
set p [file join $p $what$suffix]
if {[file exists $p]} {
return [file normalize $p]
}
}
return {}
}

proc sanitize_command_line {command_line from_index} {
set i $from_index
while {$i < [llength $command_line]} {
set cmd [lindex $command_line $i]
if {[file pathtype $cmd] ne "absolute"} {
set fullpath [_which $cmd]
if {$fullpath eq ""} {
throw {NOT-FOUND} "$cmd not found in PATH"
}
lset command_line $i $fullpath
}

# handle piped commands, e.g. `exec A | B`
for {incr i} {$i < [llength $command_line]} {incr i} {
if {[lindex $command_line $i] eq "|"} {
incr i
break
}
}
}
return $command_line
}

# Override `exec` to avoid unsafe PATH lookup

rename exec real_exec

proc exec {args} {
# skip options
for {set i 0} {$i < [llength $args]} {incr i} {
set arg [lindex $args $i]
if {$arg eq "--"} {
incr i
break
}
if {[string range $arg 0 0] ne "-"} {
break
}
}
set args [sanitize_command_line $args $i]
uplevel 1 real_exec $args
}

# Override `open` to avoid unsafe PATH lookup

rename open real_open

proc open {args} {
set arg0 [lindex $args 0]
if {[string range $arg0 0 0] eq "|"} {
set command_line [string trim [string range $arg0 1 end]]
lset args 0 "| [sanitize_command_line $command_line 0]"
}
uplevel 1 real_open $args
}

######################################################################
##
## locate our library
@ -163,8 +296,6 @@ set _isbare {}
set _gitexec {}
set _githtmldir {}
set _reponame {}
set _iscygwin {}
set _search_path {}
set _shellpath {@@SHELL_PATH@@}

set _trace [lsearch -exact $argv --trace]
@ -252,40 +383,6 @@ proc reponame {} {
return $::_reponame
}

proc is_MacOSX {} {
if {[tk windowingsystem] eq {aqua}} {
return 1
}
return 0
}

proc is_Windows {} {
if {$::tcl_platform(platform) eq {windows}} {
return 1
}
return 0
}

proc is_Cygwin {} {
global _iscygwin
if {$_iscygwin eq {}} {
if {$::tcl_platform(platform) eq {windows}} {
if {[catch {set p [exec cygpath --windir]} err]} {
set _iscygwin 0
} else {
set _iscygwin 1
# Handle MSys2 which is only cygwin when MSYSTEM is MSYS.
if {[info exists ::env(MSYSTEM)] && $::env(MSYSTEM) ne "MSYS"} {
set _iscygwin 0
}
}
} else {
set _iscygwin 0
}
}
return $_iscygwin
}

proc is_enabled {option} {
global enabled_options
if {[catch {set on $enabled_options($option)}]} {return 0}
@ -448,44 +545,6 @@ proc _git_cmd {name} {
return $v
}

proc _which {what args} {
global env _search_exe _search_path

if {$_search_path eq {}} {
if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
set _search_path [split [exec cygpath \
--windows \
--path \
--absolute \
$env(PATH)] {;}]
set _search_exe .exe
} elseif {[is_Windows]} {
set gitguidir [file dirname [info script]]
regsub -all ";" $gitguidir "\\;" gitguidir
set env(PATH) "$gitguidir;$env(PATH)"
set _search_path [split $env(PATH) {;}]
set _search_exe .exe
} else {
set _search_path [split $env(PATH) :]
set _search_exe {}
}
}

if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
set suffix {}
} else {
set suffix $_search_exe
}

foreach p $_search_path {
set p [file join $p $what$suffix]
if {[file exists $p]} {
return [file normalize $p]
}
}
return {}
}

# Test a file for a hashbang to identify executable scripts on Windows.
proc is_shellscript {filename} {
if {![file exists $filename]} {return 0}