зеркало из https://github.com/golang/blog.git
content: add path-security.article
Change-Id: I6523eb6eea5472d0a07c054bbdbb798b91320515 Reviewed-on: https://go-review.googlesource.com/c/blog/+/284832 Trust: Russ Cox <rsc@golang.org> Run-TryBot: Russ Cox <rsc@golang.org> Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
Родитель
73406574fd
Коммит
3b6722825b
|
@ -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).
|
Загрузка…
Ссылка в новой задаче