git-lfs/git/git.go

1484 lines
40 KiB
Go
Raw Normal View History

2014-09-24 17:10:29 +00:00
// Package git contains various commands that shell out to git
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
package git
import (
"bufio"
"bytes"
"crypto/sha1"
"crypto/sha256"
2017-05-25 20:38:05 +00:00
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
2016-03-17 23:15:17 +00:00
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
2015-05-13 19:43:41 +00:00
lfserrors "github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/subprocess"
"github.com/git-lfs/git-lfs/v3/tools"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/git-lfs/gitobj/v2"
"github.com/rubyist/tracerx"
)
type RefType int
const (
RefTypeLocalBranch = RefType(iota)
RefTypeRemoteBranch = RefType(iota)
RefTypeLocalTag = RefType(iota)
RefTypeRemoteTag = RefType(iota)
RefTypeHEAD = RefType(iota) // current checkout
RefTypeOther = RefType(iota) // stash or unknown
SHA1HexSize = sha1.Size * 2
SHA256HexSize = sha256.Size * 2
)
var (
ObjectIDRegex = fmt.Sprintf("(?:[0-9a-f]{%d}(?:[0-9a-f]{%d})?)", SHA1HexSize, SHA256HexSize-SHA1HexSize)
// ObjectIDLengths is a slice of valid Git hexadecimal object ID
// lengths in increasing order.
ObjectIDLengths = []int{SHA1HexSize, SHA256HexSize}
emptyTree = ""
emptyTreeMutex = &sync.Mutex{}
)
type IndexStage int
const (
IndexStageDefault IndexStage = iota
IndexStageBase
IndexStageOurs
IndexStageTheirs
)
2017-05-25 20:35:42 +00:00
// Prefix returns the given RefType's prefix, "refs/heads", "ref/remotes",
// etc. It returns an additional value of either true/false, whether or not this
// given ref type has a prefix.
//
// If the RefType is unrecognized, Prefix() will panic.
func (t RefType) Prefix() (string, bool) {
switch t {
case RefTypeLocalBranch:
return "refs/heads", true
case RefTypeRemoteBranch:
return "refs/remotes", true
case RefTypeLocalTag:
return "refs/tags", true
default:
return "", false
2017-05-25 20:35:42 +00:00
}
}
2017-10-31 21:05:42 +00:00
func ParseRef(absRef, sha string) *Ref {
r := &Ref{Sha: sha}
if strings.HasPrefix(absRef, "refs/heads/") {
r.Name = absRef[11:]
r.Type = RefTypeLocalBranch
} else if strings.HasPrefix(absRef, "refs/tags/") {
r.Name = absRef[10:]
r.Type = RefTypeLocalTag
} else if strings.HasPrefix(absRef, "refs/remotes/") {
r.Name = absRef[13:]
r.Type = RefTypeRemoteBranch
} else {
r.Name = absRef
if absRef == "HEAD" {
r.Type = RefTypeHEAD
} else {
r.Type = RefTypeOther
}
}
return r
}
// A git reference (branch, tag etc)
type Ref struct {
Name string
Type RefType
Sha string
}
2017-11-17 15:42:51 +00:00
// Refspec returns the fully-qualified reference name (including remote), i.e.,
// for a remote branch called 'my-feature' on remote 'origin', this function
// will return:
//
// refs/remotes/origin/my-feature
2017-11-17 15:42:51 +00:00
func (r *Ref) Refspec() string {
if r == nil {
return ""
}
prefix, ok := r.Type.Prefix()
if ok {
return fmt.Sprintf("%s/%s", prefix, r.Name)
}
return r.Name
}
// HasValidObjectIDLength returns true if `s` has a length that is a valid
// hexadecimal Git object ID length.
func HasValidObjectIDLength(s string) bool {
for _, length := range ObjectIDLengths {
if len(s) == length {
return true
}
}
return false
}
// IsZeroObjectID returns true if the string is a valid hexadecimal Git object
// ID and represents the all-zeros object ID for some hash algorithm.
func IsZeroObjectID(s string) bool {
for _, length := range ObjectIDLengths {
if s == strings.Repeat("0", length) {
return true
}
}
return false
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
func EmptyTree() (string, error) {
emptyTreeMutex.Lock()
defer emptyTreeMutex.Unlock()
if len(emptyTree) == 0 {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("hash-object", "-t", "tree", "/dev/null")
if err != nil {
return "", errors.New(tr.Tr.Get("failed to find `git hash-object`: %v", err))
}
cmd.Stdin = nil
out, _ := cmd.Output()
emptyTree = strings.TrimSpace(string(out))
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
return emptyTree, nil
}
// Some top level information about a commit (only first line of message)
type CommitSummary struct {
Sha string
ShortSha string
Parents []string
CommitDate time.Time
AuthorDate time.Time
AuthorName string
AuthorEmail string
CommitterName string
CommitterEmail string
Subject string
}
// Prepend Git config instructions to disable Git LFS filter
func gitConfigNoLFS(args ...string) []string {
// Before git 2.8, setting filters to blank causes lots of warnings, so use cat instead (slightly slower)
// Also pre 2.2 it failed completely. We used to use it anyway in git 2.2-2.7 and
// suppress the messages in stderr, but doing that with standard StderrPipe suppresses
// the git clone output (git thinks it's not a terminal) and makes it look like it's
// not working. You can get around that with https://github.com/kr/pty but that
// causes difficult issues with passing through Stdin for login prompts
// This way is simpler & more practical.
filterOverride := ""
if !IsGitVersionAtLeast("2.8.0") {
filterOverride = "cat"
}
return append([]string{
"-c", fmt.Sprintf("filter.lfs.smudge=%v", filterOverride),
"-c", fmt.Sprintf("filter.lfs.clean=%v", filterOverride),
"-c", "filter.lfs.process=",
"-c", "filter.lfs.required=false",
}, args...)
}
// Invoke Git with disabled LFS filters
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
func gitNoLFS(args ...string) (*subprocess.Cmd, error) {
return subprocess.ExecCommand("git", gitConfigNoLFS(args...)...)
}
func gitNoLFSSimple(args ...string) (string, error) {
return subprocess.SimpleExec("git", gitConfigNoLFS(args...)...)
}
func gitNoLFSBuffered(args ...string) (*subprocess.BufferedCmd, error) {
return subprocess.BufferedExec("git", gitConfigNoLFS(args...)...)
}
// Invoke Git with enabled LFS filters
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
func git(args ...string) (*subprocess.Cmd, error) {
return subprocess.ExecCommand("git", args...)
}
func gitSimple(args ...string) (string, error) {
return subprocess.SimpleExec("git", args...)
}
func gitBuffered(args ...string) (*subprocess.BufferedCmd, error) {
return subprocess.BufferedExec("git", args...)
}
func CatFile() (*subprocess.BufferedCmd, error) {
return gitNoLFSBuffered("cat-file", "--batch-check")
}
func DiffIndex(ref string, cached bool, refresh bool) (*bufio.Scanner, error) {
if refresh {
_, err := gitSimple("update-index", "-q", "--refresh")
if err != nil {
return nil, lfserrors.Wrap(err, tr.Tr.Get("Failed to run `git update-index`"))
}
}
args := []string{"diff-index", "-M"}
if cached {
args = append(args, "--cached")
}
args = append(args, ref)
cmd, err := gitBuffered(args...)
if err != nil {
return nil, err
}
if err = cmd.Stdin.Close(); err != nil {
return nil, err
}
return bufio.NewScanner(cmd.Stdout), nil
}
func HashObject(r io.Reader) (string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("hash-object", "--stdin")
if err != nil {
return "", errors.New(tr.Tr.Get("failed to find `git hash-object`: %v", err))
}
cmd.Stdin = r
out, err := cmd.Output()
if err != nil {
return "", errors.New(tr.Tr.Get("error building Git blob OID: %s", err))
}
return string(bytes.TrimSpace(out)), nil
}
func Log(args ...string) (*subprocess.BufferedCmd, error) {
logArgs := append([]string{"log"}, args...)
return gitNoLFSBuffered(logArgs...)
}
2015-04-24 17:43:29 +00:00
func LsRemote(remote, remoteRef string) (string, error) {
if remote == "" {
return "", errors.New(tr.Tr.Get("remote required"))
}
2015-04-24 17:43:29 +00:00
if remoteRef == "" {
return gitNoLFSSimple("ls-remote", remote)
}
return gitNoLFSSimple("ls-remote", remote, remoteRef)
}
func LsTree(ref string) (*subprocess.BufferedCmd, error) {
return gitNoLFSBuffered(
"ls-tree",
"-r", // recurse
"-l", // report object size (we'll need this)
"-z", // null line termination
"--full-tree", // start at the root regardless of where we are in it
ref,
)
}
func ResolveRef(ref string) (*Ref, error) {
outp, err := gitNoLFSSimple("rev-parse", ref, "--symbolic-full-name", ref)
if err != nil {
return nil, errors.New(tr.Tr.Get("Git can't resolve ref: %q", ref))
}
if outp == "" {
return nil, errors.New(tr.Tr.Get("Git can't resolve ref: %q", ref))
}
lines := strings.Split(outp, "\n")
fullref := &Ref{Sha: lines[0]}
if len(lines) == 1 {
// ref is a sha1 and has no symbolic-full-name
fullref.Name = lines[0]
fullref.Sha = lines[0]
fullref.Type = RefTypeOther
return fullref, nil
}
// parse the symbolic-full-name
fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1])
return fullref, nil
}
func ResolveRefs(refnames []string) ([]*Ref, error) {
refs := make([]*Ref, len(refnames))
for i, name := range refnames {
ref, err := ResolveRef(name)
if err != nil {
return refs, err
}
refs[i] = ref
}
return refs, nil
}
func CurrentRef() (*Ref, error) {
return ResolveRef("HEAD")
}
func (c *Configuration) CurrentRemoteRef() (*Ref, error) {
remoteref, err := c.RemoteRefNameForCurrentBranch()
if err != nil {
return nil, err
}
return ResolveRef(remoteref)
}
// RemoteRefForCurrentBranch returns the full remote ref (refs/remotes/{remote}/{remotebranch})
// that the current branch is tracking.
func (c *Configuration) RemoteRefNameForCurrentBranch() (string, error) {
ref, err := CurrentRef()
2014-10-27 16:52:28 +00:00
if err != nil {
return "", err
}
if ref.Type == RefTypeHEAD || ref.Type == RefTypeOther {
return "", errors.New(tr.Tr.Get("not on a branch"))
2014-10-27 16:52:28 +00:00
}
remote := c.RemoteForBranch(ref.Name)
2014-10-27 16:52:28 +00:00
if remote == "" {
return "", errors.New(tr.Tr.Get("remote not found for branch %q", ref.Name))
2014-10-27 16:52:28 +00:00
}
remotebranch := c.RemoteBranchForLocalBranch(ref.Name)
return fmt.Sprintf("refs/remotes/%s/%s", remote, remotebranch), nil
}
// RemoteForBranch returns the remote name that a given local branch is tracking (blank if none)
func (c *Configuration) RemoteForBranch(localBranch string) string {
return c.Find(fmt.Sprintf("branch.%s.remote", localBranch))
}
// RemoteBranchForLocalBranch returns the name (only) of the remote branch that the local branch is tracking
// If no specific branch is configured, returns local branch name
func (c *Configuration) RemoteBranchForLocalBranch(localBranch string) string {
// get remote ref to track, may not be same name
merge := c.Find(fmt.Sprintf("branch.%s.merge", localBranch))
if strings.HasPrefix(merge, "refs/heads/") {
return merge[11:]
} else {
return localBranch
}
2014-10-27 16:52:28 +00:00
}
func RemoteList() ([]string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("remote")
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git remote`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git remote`: %v", err))
}
cmd.Start()
2016-02-24 17:42:26 +00:00
defer cmd.Wait()
scanner := bufio.NewScanner(outp)
var ret []string
for scanner.Scan() {
ret = append(ret, strings.TrimSpace(scanner.Text()))
}
2016-02-24 17:42:26 +00:00
return ret, nil
}
func RemoteURLs(push bool) (map[string][]string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("remote", "-v")
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git remote -v`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git remote -v`: %v", err))
}
cmd.Start()
defer cmd.Wait()
scanner := bufio.NewScanner(outp)
text := "(fetch)"
if push {
text = "(push)"
}
ret := make(map[string][]string)
for scanner.Scan() {
// [remote, urlpair-text]
pair := strings.Split(strings.TrimSpace(scanner.Text()), "\t")
if len(pair) != 2 {
continue
}
// [url, "(fetch)" | "(push)"]
urlpair := strings.Split(pair[1], " ")
if len(urlpair) != 2 || urlpair[1] != text {
continue
}
ret[pair[0]] = append(ret[pair[0]], urlpair[0])
}
return ret, nil
}
func MapRemoteURL(url string, push bool) (string, bool) {
urls, err := RemoteURLs(push)
if err != nil {
return url, false
}
for name, remotes := range urls {
if len(remotes) == 1 && url == remotes[0] {
return name, true
}
}
return url, false
}
// Refs returns all of the local and remote branches and tags for the current
// repository. Other refs (HEAD, refs/stash, git notes) are ignored.
func LocalRefs() ([]*Ref, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("show-ref")
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git show-ref`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git show-ref`: %v", err))
}
var refs []*Ref
if err := cmd.Start(); err != nil {
return refs, err
}
scanner := bufio.NewScanner(outp)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
parts := strings.SplitN(line, " ", 2)
if len(parts) != 2 || !HasValidObjectIDLength(parts[0]) || len(parts[1]) < 1 {
tracerx.Printf("Invalid line from `git show-ref`: %q", line)
continue
}
rtype, name := ParseRefToTypeAndName(parts[1])
if rtype != RefTypeLocalBranch && rtype != RefTypeLocalTag {
continue
}
refs = append(refs, &Ref{name, rtype, parts[0]})
}
return refs, cmd.Wait()
}
2017-05-25 20:38:05 +00:00
// UpdateRef moves the given ref to a new sha with a given reason (and creates a
2017-05-25 21:02:16 +00:00
// reflog entry, if a "reason" was provided). It returns an error if any were
// encountered.
func UpdateRef(ref *Ref, to []byte, reason string) error {
2017-06-19 18:47:37 +00:00
return UpdateRefIn("", ref, to, reason)
}
// UpdateRef moves the given ref to a new sha with a given reason (and creates a
// reflog entry, if a "reason" was provided). It operates within the given
// working directory "wd". It returns an error if any were encountered.
func UpdateRefIn(wd string, ref *Ref, to []byte, reason string) error {
2017-11-17 15:42:51 +00:00
args := []string{"update-ref", ref.Refspec(), hex.EncodeToString(to)}
2017-05-25 20:38:05 +00:00
if len(reason) > 0 {
args = append(args, "-m", reason)
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS(args...)
if err != nil {
return errors.New(tr.Tr.Get("failed to find `git update-ref`: %v", err))
}
2017-06-19 18:47:37 +00:00
cmd.Dir = wd
return cmd.Run()
2017-05-25 20:38:05 +00:00
}
// ValidateRemote checks that a named remote is valid for use
// Mainly to check user-supplied remotes & fail more nicely
func ValidateRemote(remote string) error {
remotes, err := RemoteList()
if err != nil {
return err
}
for _, r := range remotes {
if r == remote {
return nil
}
}
2016-03-17 23:15:17 +00:00
if err = ValidateRemoteURL(remote); err == nil {
return nil
}
return errors.New(tr.Tr.Get("invalid remote name: %q", remote))
2016-03-17 23:15:17 +00:00
}
// ValidateRemoteURL checks that a string is a valid Git remote URL
func ValidateRemoteURL(remote string) error {
u, _ := url.Parse(remote)
if u == nil || u.Scheme == "" {
2016-03-17 23:15:17 +00:00
// This is either an invalid remote name (maybe the user made a typo
// when selecting a named remote) or a bare SSH URL like
// "x@y.com:path/to/resource.git". Guess that this is a URL in the latter
// form if the string contains a colon ":", and an invalid remote if it
// does not.
if strings.Contains(remote, ":") {
return nil
} else {
return errors.New(tr.Tr.Get("invalid remote name: %q", remote))
2016-03-17 23:15:17 +00:00
}
}
switch u.Scheme {
case "ssh", "http", "https", "git", "file":
return nil
2016-03-17 23:15:17 +00:00
default:
return errors.New(tr.Tr.Get("invalid remote URL protocol %q in %q", u.Scheme, remote))
2016-03-17 23:15:17 +00:00
}
}
func RewriteLocalPathAsURL(path string) string {
var slash string
if abs, err := filepath.Abs(path); err == nil {
// Required for Windows paths to work.
if !strings.HasPrefix(abs, "/") {
slash = "/"
}
path = abs
}
var gitpath string
if filepath.Base(path) == ".git" {
gitpath = path
path = filepath.Dir(path)
} else {
gitpath = filepath.Join(path, ".git")
}
if _, err := os.Stat(gitpath); err == nil {
path = gitpath
} else if _, err := os.Stat(path); err != nil {
// Not a local path. We check down here because we perform
// canonicalization by stripping off the .git above.
return path
}
return fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path))
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
func UpdateIndexFromStdin() (*subprocess.Cmd, error) {
return git("update-index", "-q", "--refresh", "--stdin")
}
// RecentBranches returns branches with commit dates on or after the given date/time
// Return full Ref type for easier detection of duplicate SHAs etc
// since: refs with commits on or after this date will be included
// includeRemoteBranches: true to include refs on remote branches
// onlyRemote: set to non-blank to only include remote branches on a single remote
func RecentBranches(since time.Time, includeRemoteBranches bool, onlyRemote string) ([]*Ref, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("for-each-ref",
`--sort=-committerdate`,
`--format=%(refname) %(objectname) %(committerdate:iso)`,
"refs")
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git for-each-ref`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git for-each-ref`: %v", err))
}
cmd.Start()
2016-02-24 17:42:26 +00:00
defer cmd.Wait()
scanner := bufio.NewScanner(outp)
// Output is like this:
// refs/heads/master f03686b324b29ff480591745dbfbbfa5e5ac1bd5 2015-08-19 16:50:37 +0100
// refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 2015-08-13 16:50:37 +0100
// Output is ordered by latest commit date first, so we can stop at the threshold
regex := regexp.MustCompile(fmt.Sprintf(`^(refs/[^/]+/\S+)\s+(%s)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}\:\d{2}\:\d{2}\s+[\+\-]\d{4})`, ObjectIDRegex))
2015-10-14 14:27:11 +00:00
tracerx.Printf("RECENT: Getting refs >= %v", since)
var ret []*Ref
for scanner.Scan() {
line := scanner.Text()
if match := regex.FindStringSubmatch(line); match != nil {
fullref := match[1]
sha := match[2]
reftype, ref := ParseRefToTypeAndName(fullref)
if reftype == RefTypeRemoteBranch {
if !includeRemoteBranches {
continue
}
if onlyRemote != "" && !strings.HasPrefix(ref, onlyRemote+"/") {
continue
}
}
// This is a ref we might use
// Check the date
commitDate, err := ParseGitDate(match[3])
if err != nil {
return ret, err
}
if commitDate.Before(since) {
// the end
break
}
2015-10-14 14:27:11 +00:00
tracerx.Printf("RECENT: %v (%v)", ref, commitDate)
ret = append(ret, &Ref{ref, reftype, sha})
}
}
return ret, nil
}
// Get the type & name of a git reference
func ParseRefToTypeAndName(fullref string) (t RefType, name string) {
const localPrefix = "refs/heads/"
const remotePrefix = "refs/remotes/"
const localTagPrefix = "refs/tags/"
if fullref == "HEAD" {
name = fullref
t = RefTypeHEAD
} else if strings.HasPrefix(fullref, localPrefix) {
name = fullref[len(localPrefix):]
t = RefTypeLocalBranch
} else if strings.HasPrefix(fullref, remotePrefix) {
name = fullref[len(remotePrefix):]
t = RefTypeRemoteBranch
} else if strings.HasPrefix(fullref, localTagPrefix) {
name = fullref[len(localTagPrefix):]
t = RefTypeLocalTag
} else {
name = fullref
t = RefTypeOther
}
return
}
// Parse a Git date formatted in ISO 8601 format (%ci/%ai)
func ParseGitDate(str string) (time.Time, error) {
// Unfortunately Go and Git don't overlap in their builtin date formats
// Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that
// when the day is < 10 Git outputs a single digit, but Go expects a leading
// zero - this is enough to break the parsing. Sigh.
// Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go
return time.Parse("2006-01-02 15:04:05 -0700", str)
}
2015-08-21 14:19:16 +00:00
// FormatGitDate converts a Go date into a git command line format date
func FormatGitDate(tm time.Time) string {
// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
}
// Get summary information about a commit
func GetCommitSummary(commit string) (*CommitSummary, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("show", "-s",
`--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit)
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git show`: %v", err))
}
out, err := cmd.CombinedOutput()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git show`: %v %v", err, string(out)))
}
// At most 10 substrings so subject line is not split on anything
fields := strings.SplitN(string(out), "|", 10)
// Cope with the case where subject is blank
if len(fields) >= 9 {
ret := &CommitSummary{}
// Get SHAs from output, not commit input, so we can support symbolic refs
ret.Sha = fields[0]
ret.ShortSha = fields[1]
ret.Parents = strings.Split(fields[2], " ")
// %aD & %cD (RFC2822) matches Go's RFC1123Z format
ret.AuthorDate, _ = ParseGitDate(fields[3])
ret.CommitDate, _ = ParseGitDate(fields[4])
ret.AuthorEmail = fields[5]
ret.AuthorName = fields[6]
ret.CommitterEmail = fields[7]
ret.CommitterName = fields[8]
if len(fields) > 9 {
ret.Subject = strings.TrimRight(fields[9], "\n")
}
return ret, nil
} else {
msg := tr.Tr.Get("Unexpected output from `git show`: %v", string(out))
return nil, errors.New(msg)
}
}
func GitAndRootDirs() (string, string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("rev-parse", "--git-dir", "--show-toplevel")
if err != nil {
return "", "", errors.New(tr.Tr.Get("failed to find `git rev-parse --git-dir --show-toplevel`: %v", err))
}
buf := &bytes.Buffer{}
cmd.Stderr = buf
out, err := cmd.Output()
output := string(out)
if err != nil {
// If we got a fatal error, it's possible we're on a newer
// (2.24+) Git and we're not in a worktree, so fall back to just
// looking up the repo directory.
if lfserrors.ExitStatus(err) == 128 {
absGitDir, err := GitDir()
return absGitDir, "", err
}
return "", "", errors.New(tr.Tr.Get("failed to call `git rev-parse --git-dir --show-toplevel`: %q", buf.String()))
}
paths := strings.Split(output, "\n")
pathLen := len(paths)
if pathLen == 0 {
return "", "", errors.New(tr.Tr.Get("bad `git rev-parse` output: %q", output))
}
absGitDir, err := tools.CanonicalizePath(paths[0], false)
if err != nil {
return "", "", errors.New(tr.Tr.Get("error converting %q to absolute: %s", paths[0], err))
}
if pathLen == 1 || len(paths[1]) == 0 {
return absGitDir, "", nil
}
absRootDir, err := tools.CanonicalizePath(paths[1], false)
return absGitDir, absRootDir, err
}
func RootDir() (string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("rev-parse", "--show-toplevel")
if err != nil {
return "", errors.New(tr.Tr.Get("failed to find `git rev-parse --show-toplevel`: %v", err))
}
out, err := cmd.Output()
if err != nil {
return "", errors.New(tr.Tr.Get("failed to call `git rev-parse --show-toplevel`: %v %v", err, string(out)))
}
path := strings.TrimSpace(string(out))
path, err = tools.TranslateCygwinPath(path)
if err != nil {
return "", err
}
return tools.CanonicalizePath(path, false)
}
2015-11-09 17:46:29 +00:00
func GitDir() (string, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("rev-parse", "--git-dir")
if err != nil {
// The %w format specifier is unique to fmt.Errorf(), so we
// do not pass it to tr.Tr.Get().
return "", fmt.Errorf("%s: %w", tr.Tr.Get("failed to find `git rev-parse --git-dir`"), err)
}
buf := &bytes.Buffer{}
cmd.Stderr = buf
out, err := cmd.Output()
if err != nil {
// The %w format specifier is unique to fmt.Errorf(), so we
// do not pass it to tr.Tr.Get().
return "", fmt.Errorf("%s: %w %v: %v", tr.Tr.Get("failed to call `git rev-parse --git-dir`"), err, string(out), buf.String())
}
path := strings.TrimSpace(string(out))
return tools.CanonicalizePath(path, false)
2015-11-09 17:46:29 +00:00
}
func GitCommonDir() (string, error) {
// Versions before 2.5.0 don't have the --git-common-dir option, since
// it came in with worktrees, so just fall back to the main Git
// directory.
if !IsGitVersionAtLeast("2.5.0") {
return GitDir()
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("rev-parse", "--git-common-dir")
if err != nil {
return "", errors.New(tr.Tr.Get("failed to find `git rev-parse --git-common-dir`: %v", err))
}
out, err := cmd.Output()
buf := &bytes.Buffer{}
cmd.Stderr = buf
if err != nil {
return "", errors.New(tr.Tr.Get("failed to call `git rev-parse --git-common-dir`: %v %v: %v", err, string(out), buf.String()))
}
path := strings.TrimSpace(string(out))
path, err = tools.TranslateCygwinPath(path)
if err != nil {
return "", err
}
return tools.CanonicalizePath(path, false)
}
// GetAllWorkTreeHEADs returns the refs that all worktrees are using as HEADs
// This returns all worktrees plus the master working copy, and works even if
// working dir is actually in a worktree right now
// Pass in the git storage dir (parent of 'objects') to work from
func GetAllWorkTreeHEADs(storageDir string) ([]*Ref, error) {
worktreesdir := filepath.Join(storageDir, "worktrees")
dirf, err := os.Open(worktreesdir)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
var worktrees []*Ref
if err == nil {
// There are some worktrees
defer dirf.Close()
direntries, err := dirf.Readdir(0)
if err != nil {
return nil, err
}
for _, dirfi := range direntries {
if dirfi.IsDir() {
// to avoid having to chdir and run git commands to identify the commit
// just read the HEAD file & git rev-parse if necessary
// Since the git repo is shared the same rev-parse will work from this location
headfile := filepath.Join(worktreesdir, dirfi.Name(), "HEAD")
ref, err := parseRefFile(headfile)
if err != nil {
tracerx.Printf("Error reading %v for worktree, skipping: %v", headfile, err)
continue
}
worktrees = append(worktrees, ref)
}
}
}
// This has only established the separate worktrees, not the original checkout
// If the storageDir contains a HEAD file then there is a main checkout
2022-01-05 06:49:08 +00:00
// as well; this must be resolveable whether you're in the main checkout or
// a worktree
headfile := filepath.Join(storageDir, "HEAD")
ref, err := parseRefFile(headfile)
if err == nil {
worktrees = append(worktrees, ref)
} else if !os.IsNotExist(err) { // ok if not exists, probably bare repo
tracerx.Printf("Error reading %v for main checkout, skipping: %v", headfile, err)
}
return worktrees, nil
}
// Manually parse a reference file like HEAD and return the Ref it resolves to
func parseRefFile(filename string) (*Ref, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
contents := strings.TrimSpace(string(bytes))
if strings.HasPrefix(contents, "ref:") {
contents = strings.TrimSpace(contents[4:])
}
return ResolveRef(contents)
}
// IsBare returns whether or not a repository is bare. It requires that the
// current working directory is a repository.
//
// If there was an error determining whether or not the repository is bare, it
// will be returned.
func IsBare() (bool, error) {
s, err := subprocess.SimpleExec(
"git", "rev-parse", "--is-bare-repository")
if err != nil {
return false, err
}
return strconv.ParseBool(s)
}
// For compatibility with git clone we must mirror all flags in CloneWithoutFilters
type CloneFlags struct {
// --template <template_directory>
TemplateDirectory string
// -l --local
Local bool
// -s --shared
Shared bool
// --no-hardlinks
NoHardlinks bool
// -q --quiet
Quiet bool
// -n --no-checkout
NoCheckout bool
// --progress
Progress bool
// --bare
Bare bool
// --mirror
Mirror bool
// -o <name> --origin <name>
Origin string
// -b <name> --branch <name>
Branch string
// -u <upload-pack> --upload-pack <pack>
Upload string
// --reference <repository>
Reference string
// --reference-if-able <repository>
ReferenceIfAble string
// --dissociate
Dissociate bool
// --separate-git-dir <git dir>
SeparateGit string
// --depth <depth>
Depth string
// --recursive
Recursive bool
// --recurse-submodules
RecurseSubmodules bool
// -c <value> --config <value>
Config string
// --single-branch
SingleBranch bool
// --no-single-branch
NoSingleBranch bool
2016-04-19 11:01:49 +00:00
// --verbose
Verbose bool
// --ipv4
Ipv4 bool
// --ipv6
Ipv6 bool
// --shallow-since <date>
ShallowSince string
// --shallow-since <date>
ShallowExclude string
// --shallow-submodules
ShallowSubmodules bool
// --no-shallow-submodules
NoShallowSubmodules bool
// jobs <n>
Jobs int64
}
2016-02-05 17:45:12 +00:00
// CloneWithoutFilters clones a git repo but without the smudge filter enabled
// so that files in the working copy will be pointers and not real LFS data
func CloneWithoutFilters(flags CloneFlags, args []string) error {
2016-02-05 17:45:12 +00:00
cmdargs := []string{"clone"}
// flags
if flags.Bare {
cmdargs = append(cmdargs, "--bare")
}
if len(flags.Branch) > 0 {
cmdargs = append(cmdargs, "--branch", flags.Branch)
}
if len(flags.Config) > 0 {
cmdargs = append(cmdargs, "--config", flags.Config)
}
if len(flags.Depth) > 0 {
cmdargs = append(cmdargs, "--depth", flags.Depth)
}
if flags.Dissociate {
cmdargs = append(cmdargs, "--dissociate")
}
2016-04-19 11:01:49 +00:00
if flags.Ipv4 {
cmdargs = append(cmdargs, "--ipv4")
}
if flags.Ipv6 {
cmdargs = append(cmdargs, "--ipv6")
}
if flags.Local {
cmdargs = append(cmdargs, "--local")
}
if flags.Mirror {
cmdargs = append(cmdargs, "--mirror")
}
if flags.NoCheckout {
cmdargs = append(cmdargs, "--no-checkout")
}
if flags.NoHardlinks {
cmdargs = append(cmdargs, "--no-hardlinks")
}
if flags.NoSingleBranch {
cmdargs = append(cmdargs, "--no-single-branch")
}
if len(flags.Origin) > 0 {
cmdargs = append(cmdargs, "--origin", flags.Origin)
}
if flags.Progress {
cmdargs = append(cmdargs, "--progress")
}
if flags.Quiet {
cmdargs = append(cmdargs, "--quiet")
}
if flags.Recursive {
cmdargs = append(cmdargs, "--recursive")
}
if flags.RecurseSubmodules {
cmdargs = append(cmdargs, "--recurse-submodules")
}
if len(flags.Reference) > 0 {
cmdargs = append(cmdargs, "--reference", flags.Reference)
}
if len(flags.ReferenceIfAble) > 0 {
cmdargs = append(cmdargs, "--reference-if-able", flags.ReferenceIfAble)
}
if len(flags.SeparateGit) > 0 {
cmdargs = append(cmdargs, "--separate-git-dir", flags.SeparateGit)
}
if flags.Shared {
cmdargs = append(cmdargs, "--shared")
}
if flags.SingleBranch {
cmdargs = append(cmdargs, "--single-branch")
}
if len(flags.TemplateDirectory) > 0 {
cmdargs = append(cmdargs, "--template", flags.TemplateDirectory)
}
if len(flags.Upload) > 0 {
cmdargs = append(cmdargs, "--upload-pack", flags.Upload)
}
2016-04-19 11:01:49 +00:00
if flags.Verbose {
cmdargs = append(cmdargs, "--verbose")
}
if len(flags.ShallowSince) > 0 {
cmdargs = append(cmdargs, "--shallow-since", flags.ShallowSince)
}
if len(flags.ShallowExclude) > 0 {
cmdargs = append(cmdargs, "--shallow-exclude", flags.ShallowExclude)
}
if flags.ShallowSubmodules {
cmdargs = append(cmdargs, "--shallow-submodules")
}
if flags.NoShallowSubmodules {
cmdargs = append(cmdargs, "--no-shallow-submodules")
}
if flags.Jobs > -1 {
cmdargs = append(cmdargs, "--jobs", strconv.FormatInt(flags.Jobs, 10))
}
// Now args
2016-02-05 17:45:12 +00:00
cmdargs = append(cmdargs, args...)
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS(cmdargs...)
if err != nil {
return errors.New(tr.Tr.Get("failed to find `git clone`: %v", err))
}
2016-02-05 17:45:12 +00:00
// Assign all streams direct
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
err = cmd.Start()
2016-02-05 17:45:12 +00:00
if err != nil {
return errors.New(tr.Tr.Get("failed to start `git clone`: %v", err))
2016-02-05 17:45:12 +00:00
}
err = cmd.Wait()
if err != nil {
return errors.New(tr.Tr.Get("`git clone` failed: %v", err))
2016-02-05 17:45:12 +00:00
}
return nil
}
2017-05-26 18:23:34 +00:00
// Checkout performs an invocation of `git-checkout(1)` applying the given
// treeish, paths, and force option, if given.
//
// If any error was encountered, it will be returned immediately. Otherwise, the
// checkout has occurred successfully.
func Checkout(treeish string, paths []string, force bool) error {
args := []string{"checkout"}
if force {
args = append(args, "--force")
}
if len(treeish) > 0 {
args = append(args, treeish)
}
if len(paths) > 0 {
args = append(args, append([]string{"--"}, paths...)...)
}
_, err := gitNoLFSSimple(args...)
2017-05-26 18:23:34 +00:00
return err
}
// CachedRemoteRefs returns the list of branches & tags for a remote which are
// currently cached locally. No remote request is made to verify them.
func CachedRemoteRefs(remoteName string) ([]*Ref, error) {
var ret []*Ref
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("show-ref")
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git show-ref`: %v", err))
}
2016-02-24 16:41:15 +00:00
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git show-ref`: %v", err))
}
cmd.Start()
scanner := bufio.NewScanner(outp)
2016-02-24 16:41:15 +00:00
refPrefix := fmt.Sprintf("refs/remotes/%v/", remoteName)
for scanner.Scan() {
if sha, name, ok := parseShowRefLine(refPrefix, scanner.Text()); ok {
// Don't match head
2016-03-01 10:06:32 +00:00
if name == "HEAD" {
continue
2016-02-24 16:41:15 +00:00
}
2016-03-01 10:06:32 +00:00
ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
2016-02-24 16:41:15 +00:00
}
}
return ret, cmd.Wait()
}
2016-02-24 16:41:15 +00:00
func parseShowRefLine(refPrefix, line string) (sha, name string, ok bool) {
// line format: <sha> <space> <ref>
space := strings.IndexByte(line, ' ')
if space < 0 {
return "", "", false
}
ref := line[space+1:]
if !strings.HasPrefix(ref, refPrefix) {
return "", "", false
}
return line[:space], strings.TrimSpace(ref[len(refPrefix):]), true
}
// Fetch performs a fetch with no arguments against the given remotes.
func Fetch(remotes ...string) error {
if len(remotes) == 0 {
return nil
}
var args []string
if len(remotes) > 1 {
args = []string{"--multiple", "--"}
}
args = append(args, remotes...)
_, err := gitNoLFSSimple(append([]string{"fetch"}, args...)...)
return err
}
// RemoteRefs returns a list of branches & tags for a remote by actually
// accessing the remote via git ls-remote.
func RemoteRefs(remoteName string) ([]*Ref, error) {
var ret []*Ref
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS("ls-remote", "--heads", "--tags", "-q", remoteName)
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git ls-remote`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git ls-remote`: %v", err))
}
cmd.Start()
scanner := bufio.NewScanner(outp)
for scanner.Scan() {
if sha, ns, name, ok := parseLsRemoteLine(scanner.Text()); ok {
// Don't match head
2016-03-01 10:06:32 +00:00
if name == "HEAD" {
continue
}
typ := RefTypeRemoteBranch
if ns == "tags" {
typ = RefTypeRemoteTag
}
ret = append(ret, &Ref{name, typ, sha})
2016-02-24 16:41:15 +00:00
}
}
return ret, cmd.Wait()
2016-02-24 16:41:15 +00:00
}
func parseLsRemoteLine(line string) (sha, ns, name string, ok bool) {
const headPrefix = "refs/heads/"
const tagPrefix = "refs/tags/"
// line format: <sha> <tab> <ref>
tab := strings.IndexByte(line, '\t')
if tab < 0 {
return "", "", "", false
}
ref := line[tab+1:]
switch {
case strings.HasPrefix(ref, headPrefix):
ns = "heads"
name = ref[len(headPrefix):]
case strings.HasPrefix(ref, tagPrefix):
ns = "tags"
name = ref[len(tagPrefix):]
default:
return "", "", "", false
}
return line[:tab], ns, strings.TrimSpace(name), true
}
// AllRefs returns a slice of all references in a Git repository in the current
// working directory, or an error if those references could not be loaded.
2017-06-19 17:58:59 +00:00
func AllRefs() ([]*Ref, error) {
return AllRefsIn("")
}
// AllRefs returns a slice of all references in a Git repository located in a
// the given working directory "wd", or an error if those references could not
// be loaded.
func AllRefsIn(wd string) ([]*Ref, error) {
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS(
"for-each-ref", "--format=%(objectname)%00%(refname)")
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
if err != nil {
return nil, lfserrors.Wrap(err, tr.Tr.Get("failed to find `git for-each-ref`: %v", err))
}
cmd.Dir = wd
2017-06-19 17:58:59 +00:00
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, lfserrors.Wrap(err, tr.Tr.Get("cannot open pipe"))
2017-06-19 17:58:59 +00:00
}
cmd.Start()
refs := make([]*Ref, 0)
scanner := bufio.NewScanner(outp)
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), "\x00", 2)
2017-06-19 17:58:59 +00:00
if len(parts) != 2 {
return nil, lfserrors.New(tr.Tr.Get(
"invalid `git for-each-ref` line: %q", scanner.Text()))
2017-06-19 17:58:59 +00:00
}
sha := parts[0]
typ, name := ParseRefToTypeAndName(parts[1])
2017-06-19 17:58:59 +00:00
refs = append(refs, &Ref{
Name: name,
Type: typ,
Sha: sha,
})
}
if err := scanner.Err(); err != nil {
return nil, err
}
return refs, nil
}
// GetTrackedFiles returns a list of files which are tracked in Git which match
// the pattern specified (standard wildcard form)
// Both pattern and the results are relative to the current working directory, not
// the root of the repository
func GetTrackedFiles(pattern string) ([]string, error) {
safePattern := sanitizePattern(pattern)
2016-10-07 17:02:18 +00:00
rootWildcard := len(safePattern) < len(pattern) && strings.ContainsRune(safePattern, '*')
var ret []string
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS(
"-c", "core.quotepath=false", // handle special chars in filenames
"ls-files",
"--cached", // include things which are staged but not committed right now
"--", // no ambiguous patterns
safePattern)
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git ls-files`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git ls-files`: %v", err))
}
cmd.Start()
scanner := bufio.NewScanner(outp)
for scanner.Scan() {
line := scanner.Text()
2016-10-07 17:02:18 +00:00
// If the given pattern is a root wildcard, skip all files which
// are not direct descendants of the repository's root.
//
// This matches the behavior of how .gitattributes performs
// filename matches.
if rootWildcard && filepath.Dir(line) != "." {
continue
}
ret = append(ret, strings.TrimSpace(line))
}
2016-04-04 20:41:43 +00:00
return ret, cmd.Wait()
}
func sanitizePattern(pattern string) string {
if strings.HasPrefix(pattern, "/") {
return pattern[1:]
}
return pattern
}
// GetFilesChanged returns a list of files which were changed, either between 2
// commits, or at a single commit if you only supply one argument and a blank
// string for the other
func GetFilesChanged(from, to string) ([]string, error) {
var files []string
args := []string{
"-c", "core.quotepath=false", // handle special chars in filenames
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
}
if len(from) > 0 {
args = append(args, from)
}
if len(to) > 0 {
args = append(args, to)
}
args = append(args, "--") // no ambiguous patterns
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := gitNoLFS(args...)
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to find `git diff-tree`: %v", err))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.New(tr.Tr.Get("failed to call `git diff-tree`: %v", err))
}
if err := cmd.Start(); err != nil {
return nil, errors.New(tr.Tr.Get("failed to start `git diff-tree`: %v", err))
}
scanner := bufio.NewScanner(outp)
for scanner.Scan() {
files = append(files, strings.TrimSpace(scanner.Text()))
}
if err := cmd.Wait(); err != nil {
return nil, errors.New(tr.Tr.Get("`git diff-tree` failed: %v", err))
}
return files, err
}
// IsFileModified returns whether the filepath specified is modified according
// to `git status`. A file is modified if it has uncommitted changes in the
// working copy or the index. This includes being untracked.
func IsFileModified(filepath string) (bool, error) {
args := []string{
"-c", "core.quotepath=false", // handle special chars in filenames
"status",
"--porcelain",
"--", // separator in case filename ambiguous
filepath,
}
subprocess: report errors when finding executables On Windows, when spawning a process, Go first looks for an executable file with the correct name in the current directory, and only if it fails to find one there does it look in the directories listed in the PATH environment variable. We would prefer not to replicate this behaviour and instead search only in the directories in PATH. Therefore, starting with the mitigation of CVE-2020-27955 in commit 74d5f2397f9abe4834bf1fe1fa02fd6c141b77ce, we resolve paths to executables ourselves rather than rely on Go to do so. The subprocess.LookPath() function we introduced in that change is adapted from Go's os/exec package. When it cannot find an executable in PATH, it returns an empty path string and an exec.ErrNotFound error. When that happens, we do not detect the condition and simply set the command path to the empty string. This can lead to undesirable behaviour in which a bug in the Go os/exec library causes it to run another executable other than the one we intended. When we set the command path to the empty string and then ask to execute the command, the native Go version of the LookPath() function for Windows is run with an empty path because filepath.Base() returns "." when passed the empty string and because we have left the current working directory also set to an empty string: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L348-L353 Since the path string does not contain any path separator characters the current working directory is searched to try to find a file with a matching name and executable file extension (e.g., ".exe" or ".com"). To search the current working directory, the "." directory name is prepended to the given path with filepath.Join(): https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L84 The filepath.Join() function ignores empty arguments, so this results in an incorrect filename of ".". All potential executable file extensions from the PATHEXT environment variable (or from a fixed list if that variable is not defined) are then appended to this filename, including their leading dot separator characters, thus producing filenames like "..com" and "..exe": https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/lp_windows.go#L46-L50 Should a file with one of these names exist in the current working directory, its name will be returned, which means that it will be executed instead of the command we expected to run. This is obviously undesirable and presents a risk to users. (Note that batch files named "..bat" and "..cmd" will also be found in the same manner, but they will not actually be executed due to an undocumented feature of the Windows API's family of CreateProcess*() functions, which are used by Go to spawn processes. When passed an lpApplicationName argument ending with a ".bat" or ".cmd" extension, the CreateProcess*() functions appear to instead execute "cmd.exe" and construct a "/c" command string from the lpCommandLine argument. The value of that argument is set using the full command line we are attempting to execute, and as such its first element is the actual name of the executable we intended to run; therefore, the command interpreter attempts to run the executable as a batch script and fails, and the "..bat" or "..cmd" file is effectively ignored.) To recap, when Git LFS attempts to execute a program on Windows, if the executable is not found anywhere in PATH but does exist in the current working directory, then when we call exec.Command() Go's internal LookPath() function will find the executable and not set the internal lookPathErr flag: https://github.com/golang/go/blob/1724077b789ad92972ab1ac03788389645306cbb/src/os/exec/exec.go#L174-L179 Since we do not want to run executables in the current working directory, we subsequently run our own LookPath() function, which we use to reset the cmd.Path field. However, when our LookPath() returns an empty string, we do not detect that and instead set cmd.Path to that value. Then when we ask Go to run the command, because lookPathErr is nil, it proceeds, and the empty path causes it to find and run the first matching file in the working directory named "..com" or "..exe" (or any similar name with an executable file extension except for "..bat" and "..cmd"). We can prevent this behaviour by detecting when our LookPath() function returns an error and propagating it upwards to all callers of our subprocess.ExecCommand() function. We also add checks for this error at appropriate points and log or report the error as necessary. One particular circumstance of note occurs when a user has a Cygwin-installed "uname" in their PATH but not "cygpath", but "cygpath.exe" does exist in their current working directory. Then we will attempt to execute "cygpath" because we use the presence of "uname" as an indication that Cygwin is fully installed. Should a "..exe" or similar file also be present in the working directory, then it will be executed instead of "cygpath.exe". As with all other instances where we call subprocess.ExecCommand(), in tools.translateCygwinPath() we will now check for a returned error before trying to actually execute "cygpath". Unlike many of the other cases, though, we do not need to report the error in this one; instead, we simply return the current path from translateCygwinPath() instead of canonicalizing it, just as we do already if the "cygpath" executable fails for some reason. Finally, we add a new test to t/t-path.sh which checks for the incorrect execution of a "..exe" binary on Windows when "git.exe" is not found in PATH but does exist in the current working directory. This test passes when run with a Git LFS binary that includes the remediations from this commit, and fails otherwise. For our "malicious" binary named "..exe" we make use of the lfstest-badpathcheck test helper we added in a previous commit. We only run this test on Windows because the underlying bug in Go is Windows-specific as it depends on path extensions from PATHEXT being appended to the file name ".". Co-authored-by: brian m. carlson <bk2204@github.com>
2022-03-29 06:16:16 +00:00
cmd, err := git(args...)
if err != nil {
return false, lfserrors.Wrap(err, tr.Tr.Get("failed to find `git status`"))
}
outp, err := cmd.StdoutPipe()
if err != nil {
return false, lfserrors.Wrap(err, tr.Tr.Get("Failed to call `git status`"))
}
if err := cmd.Start(); err != nil {
return false, lfserrors.Wrap(err, tr.Tr.Get("Failed to start `git status`"))
}
matched := false
2017-02-03 10:02:09 +00:00
for scanner := bufio.NewScanner(outp); scanner.Scan(); {
line := scanner.Text()
// Porcelain format is "<I><W> <filename>"
// Where <I> = index status, <W> = working copy status
if len(line) > 3 {
// Double-check even though should be only match
if strings.TrimSpace(line[3:]) == filepath {
matched = true
// keep consuming output to exit cleanly
// will typically fall straight through anyway due to 1 line output
}
}
}
if err := cmd.Wait(); err != nil {
return false, lfserrors.Wrap(err, tr.Tr.Get("`git status` failed"))
}
return matched, nil
}
// IsWorkingCopyDirty returns true if and only if the working copy in which the
// command was executed is dirty as compared to the index.
//
// If the status of the working copy could not be determined, an error will be
// returned instead.
func IsWorkingCopyDirty() (bool, error) {
bare, err := IsBare()
if bare || err != nil {
return false, err
}
out, err := gitSimple("status", "--porcelain")
if err != nil {
return false, err
}
return len(out) != 0, nil
}
func ObjectDatabase(osEnv, gitEnv Environment, gitdir, tempdir string) (*gitobj.ObjectDatabase, error) {
var options []gitobj.Option
objdir, ok := osEnv.Get("GIT_OBJECT_DIRECTORY")
if !ok {
objdir = filepath.Join(gitdir, "objects")
}
alternates, _ := osEnv.Get("GIT_ALTERNATE_OBJECT_DIRECTORIES")
if alternates != "" {
options = append(options, gitobj.Alternates(alternates))
}
hashAlgo, _ := gitEnv.Get("extensions.objectformat")
if hashAlgo != "" {
options = append(options, gitobj.ObjectFormat(gitobj.ObjectFormatAlgorithm(hashAlgo)))
}
odb, err := gitobj.FromFilesystem(objdir, tempdir, options...)
if err != nil {
return nil, err
}
if odb.Hasher() == nil {
return nil, errors.New(tr.Tr.Get("unsupported repository hash algorithm %q", hashAlgo))
}
return odb, nil
}