diff --git a/content/path-security.article b/content/path-security.article new file mode 100644 index 0000000..196b45c --- /dev/null +++ b/content/path-security.article @@ -0,0 +1,300 @@ +# Command PATH security in Go +19 Jan 2021 +Summary: How to decide if your programs are vulnerable to PATH problems, and what to do about it. + +Russ Cox + +## + +Today’s [Go security release](https://golang.org/s/go-security-release-jan-2021) +fixes an issue involving PATH lookups in untrusted directories +that can lead to remote execution during the `go` `get` command. +We expect people to have questions about what exactly this means +and whether they might have issues in their own programs. +This post details the bug, the fixes we have applied, +how to decide whether your own programs are vulnerable to similar problems, +and what you can do if they are. + +## Go command & remote execution + +One of the design goals for the `go` command is that most commands – including +`go` `build`, `go` `doc`, `go` `get`, `go` `install`, and `go` `list` – do not run +arbitrary code downloaded from the internet. +There are a few obvious exceptions: +clearly `go` `run`, `go` `test`, and `go` `generate` _do_ run arbitrary code – that's their job. +But the others must not, for a variety of reasons including reproducible builds and security. +So when `go` `get` can be tricked into executing arbitrary code, we consider that a security bug. + +If `go` `get` must not run arbitrary code, then unfortunately that means +all the programs it invokes, such as compilers and version control systems, are also inside the security perimeter. +For example, we've had issues in the past in which clever use of obscure compiler features +or remote execution bugs in version control systems became remote execution bugs in Go. +(On that note, Go 1.16 aims to improve the situation by introducing a GOVCS setting +that allows configuration of exactly which version control systems are allowed and when.) + +Today's bug, however, was entirely our fault, not a bug or obscure feature of `gcc` or `git`. +The bug involves how Go and other programs find other executables, +so we need to spend a little time looking at that before we can get to the details. + +## Commands and PATHs and Go + +All operating systems have a concept of an executable path +(`$PATH` on Unix, `%PATH%` on Windows; for simplicity, we'll just use the term PATH), +which is a list of directories. +When you type a command into a shell prompt, +the shell looks in each of the listed directories, +in turn, for an executable with the name you typed. +It runs the first one it finds, or it prints a message like “command not found.” + +On Unix, this idea first appeared in Seventh Edition Unix's Bourne shell (1979). The manual explained: + +> The shell parameter `$PATH` defines the search path for the directory containing the command. +> Each alternative directory name is separated by a colon (`:`). +> The default path is `:/bin:/usr/bin`. +> If the command name contains a / then the search path is not used. +> Otherwise, each directory in the path is searched for an executable file. + +Note the default: the current directory (denoted here by an empty string, +but let's call it “dot”) +is listed ahead of `/bin` and `/usr/bin`. +MS-DOS and then Windows chose to hard-code that behavior: +on those systems, dot is always searched first, +automatically, before considering any directories listed in `%PATH%`. + +As Grampp and Morris pointed out in their +classic paper “[UNIX Operating System Security](https://people.engr.ncsu.edu/gjin2/Classes/246/Spring2019/Security.pdf)” (1984), +placing dot ahead of system directories in the PATH +means that if you `cd` into a directory and run `ls`, +you might get a malicious copy from that directory +instead of the system utility. +And if you can trick a system administrator to run `ls` in your home directory +while logged in as `root`, then you can run any code you want. +Because of this problem and others like it, +essentially all modern Unix distributions set a new user's default PATH +to exclude dot. +But Windows systems continue to search dot first, no matter what PATH says. + +For example, when you type the command + + go version + +on a typically-configured Unix, +the shell runs a `go` executable from a system directory in your PATH. +But when you type that command on Windows, +`cmd.exe` checks dot first. +If `.\go.exe` (or `.\go.bat` or many other choices) exists, +`cmd.exe` runs that executable, not one from your PATH. + +For Go, PATH searches are handled by [`exec.LookPath`](https://pkg.go.dev/os/exec#LookPath), +called automatically by +[`exec.Command`](https://pkg.go.dev/os/exec#Command). +And to fit well into the host system, Go's `exec.LookPath` +implements the Unix rules on Unix and the Windows rules on Windows. +For example, this command + + out, err := exec.Command("go", "version").CombinedOutput() + +behaves the same as typing `go` `version` into the operating system shell. +On Windows, it runs `.\go.exe` when that exists. + +(It is worth noting that Windows PowerShell changed this behavior, +dropping the implicit search of dot, but `cmd.exe` and the +Windows C library [`SearchPath function`](https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-searchpatha) +continue to behave as they always have. +Go continues to match `cmd.exe`.) + +## The Bug + +When `go` `get` downloads and builds a package that contains +`import` `"C"`, it runs a program called `cgo` to prepare the Go +equivalent of the relevant C code. +The `go` command runs `cgo` in the directory containing the package sources. +Once `cgo` has generated its Go output files, +the `go` command itself invokes the Go compiler +on the generated Go files +and the host C compiler (`gcc` or `clang`) +to build any C sources included with the package. +All this works well. +But where does the `go` command find the host C compiler? +It looks in the PATH, of course. Luckily, while it runs the C compiler +in the package source directory, it does the PATH lookup +from the original directory where the `go` command was invoked: + + cmd := exec.Command("gcc", "file.c") + cmd.Dir = "badpkg" + cmd.Run() + +So even if `badpkg\gcc.exe` exists on a Windows system, +this code snippet will not find it. +The lookup that happens in `exec.Command` does not know +about the `badpkg` directory. + +The `go` command uses similar code to invoke `cgo`, +and in that case there's not even a path lookup, +because `cgo` always comes from GOROOT: + + cmd := exec.Command(GOROOT+"/pkg/tool/"+GOOS_GOARCH+"/cgo", "file.go") + cmd.Dir = "badpkg" + cmd.Run() + +This is even safer than the previous snippet: +there's no chance of running any bad `cgo.exe` that may exist. + +But it turns out that cgo itself also invokes the host C compiler, +on some temporary files it creates, meaning it executes this code itself: + + // running in cgo in badpkg dir + cmd := exec.Command("gcc", "tmpfile.c") + cmd.Run() + +Now, because cgo itself is running in `badpkg`, +not in the directory where the `go` command was run, +it will run `badpkg\gcc.exe` if that file exists, +instead of finding the system `gcc`. + +So an attacker can create a malicious package that uses cgo and +includes a `gcc.exe`, and then any Windows user +that runs `go` `get` to download and build the attacker's package +will run the attacker-supplied `gcc.exe` in preference to any +`gcc` in the system path. + +Unix systems avoid the problem first because dot is typically not +in the PATH and second because module unpacking does not +set execute bits on the files it writes. +But Unix users who have dot ahead of system directories +in their PATH and are using GOPATH mode would be as susceptible +as Windows users. +(If that describes you, today is a good day to remove dot from your path +and to start using Go modules.) + +(Thanks to [RyotaK](https://twitter.com/ryotkak) for [reporting this issue](https://golang.org/security) to us.) + +## The Fixes + +It's obviously unacceptable for the `go` `get` command to download +and run a malicious `gcc.exe`. +But what's the actual mistake that allows that? +And then what's the fix? + +One possible answer is that the mistake is that `cgo` does the search for the host C compiler +in the untrusted source directory instead of in the directory where the `go` command +was invoked. +If that's the mistake, +then the fix is to change the `go` command to pass `cgo` the full path to the +host C compiler, so that `cgo` need not do a PATH lookup in +to the untrusted directory. + +Another possible answer is that the mistake is to look in dot +during PATH lookups, whether happens automatically on Windows +or because of an explicit PATH entry on a Unix system. +A user may want to look in dot to find a command they typed +in a console or shell window, +but it's unlikely they also want to look there to find a subprocess of a subprocess +of a typed command. +If that's the mistake, +then the fix is to change the `cgo` command not to look in dot during a PATH lookup. + +We decided both were mistakes, so we applied both fixes. +The `go` command now passes the full host C compiler path to `cgo`. +On top of that, `cgo`, `go`, and every other command in the Go distribution +now use a variant of the `os/exec` package that reports an error if it would +have previously used an executable from dot. +The packages `go/build` and `go/import` use the same policy for +their invocation of the `go` command and other tools. +This should shut the door on any similar security problems that may be lurking. + +Out of an abundance of caution, we also made a similar fix in +commands like `goimports` and `gopls`, +as well as the libraries +`golang.org/x/tools/go/analysis` +and +`golang.org/x/tools/go/packages`, +which invoke the `go` command as a subprocess. +If you run these programs in untrusted directories – +for example, if you `git` `checkout` untrusted repositories +and `cd` into them and then run programs like these, +and you use Windows or use Unix with dot in your PATH – +then you should update your copies of these commands too. +If the only untrusted directories on your computer +are the ones in the module cache managed by `go` `get`, +then you only need the new Go release. + +After updating to the new Go release, you can update to the latest `gopls` by using: + + GO111MODULE=on \ + go get golang.org/x/tools/gopls@v0.6.4 + +and you can update to the latest `goimports` or other tools by using: + + GO111MODULE=on \ + go get golang.org/x/tools/cmd/goimports@v0.1.0 + +You can update programs that depend on `golang.org/x/tools/go/packages`, +even before their authors do, +by adding an explicit upgrade of the dependency during `go` `get`: + + GO111MODULE=on \ + go get example.com/cmd/thecmd golang.org/x/tools@v0.1.0 + +For programs that use `go/build`, it is sufficient for you to recompile them +using the updated Go release. + +Again, you only need to update these other programs if you +are a Windows user or a Unix user with dot in the PATH +_and_ you run these programs in source directories you do not trust +that may contain malicious programs. + +## Are your own programs affected? + +If you use `exec.LookPath` or `exec.Command` in your own programs, +you only need to be concerned if you (or your users) run your program +in a directory with untrusted contents. +If so, then a subprocess could be started using an executable +from dot instead of from a system directory. +(Again, using an executable from dot happens always on Windows +and only with uncommon PATH settings on Unix.) + +If you are concerned, then we've published the more restricted variant +of `os/exec` as [`golang.org/x/sys/execabs`](https://pkg.go.dev/golang.org/x/sys/execabs). +You can use it in your program by simply replacing + + import "os/exec" + +with + + import exec "golang.org/x/sys/execabs" + +and recompiling. + +## Securing os/exec by default + +We have been discussing on +[golang.org/issue/38736](https://golang.org/issue/38736) +whether the Windows behavior of always preferring the current directory +in PATH lookups (during `exec.Command` and `exec.LookPath`) +should be changed. +The argument in favor of the change is that it closes the kinds of +security problems discussed in this blog post. +A supporting argument is that although the Windows `SearchPath` API +and `cmd.exe` still always search the current directory, +PowerShell, the successor to `cmd.exe`, does not, +an apparent recognition that the original behavior was a mistake. +The argument against the change is that it could break existing Windows +programs that intend to find programs in the current directory. +We don’t know how many such programs exist, +but they would get unexplained failures if the PATH lookups +started skipping the current directory entirely. + +The approach we have taken in `golang.org/x/sys/execabs` may +be a reasonable middle ground. +It finds the result of the old PATH lookup and then returns a +clear error rather than use a result from the current directory. +The error returned from `exec.Command("prog")` when `prog.exe` exists looks like: + + prog resolves to executable in current directory (.\prog.exe) + +For programs that do change behavior, this error should make very clear what has happened. +Programs that intend to run a program from the current directory can use +`exec.Command("./prog")` instead (that syntax works on all systems, even Windows). + +We have filed this idea as a new proposal, [golang.org/issue/43724](https://golang.org/issue/43724).