go: os/exec: return error when PATH lookup would use current directory
We recently published a new package golang.org/x/sys/execabs, which is a forwarding wrapper around os/exec that makes three changes:
-
execabs.LookPath changes the result of exec.LookPath in the case where a PATH search returns an executable in the current directory (dot). In that case, execabs.LookPath returns an error instead. The error text is “prog resolves to executable in current directory (./prog)”
-
execabs.Command changes the result of exec.Command in the same case. It arranges that the subsequent cmd.Run or cmd.Start will return that same error.
-
execabs.CommandContext does the same.
As yet another possible answer in the “what to do about PATH lookups on Windows” saga, perhaps we should change os/exec to do these things by default. I am filing this proposal to help us discuss and decide whether to take this path. (For more background, see the blog post https://blog.golang.org/path-security.)
The proposal can be summarized as “replace os/exec with golang.org/x/sys/execabs”.
To recap how we got here, the fundamental problem is that lots of Go programs do things like exec.Command("prog") and expect that prog will come from a system directory listed in the PATH. It is a surprise and in some cases a security problem that on Windows prog will be taken from dot instead when prog.exe (or prog.bat, prog.com, …) exists. The same is true on Unix for users who have an explicit dot or empty-string element in their PATH.
We already have three issues with possible approaches to solving this problem. They are:
- #38736, which removes the implicit dot lookup from LookPath on Windows
- #42420, to add LookPathAbs that doesn’t return relative results.
- #42950, to make exec.Command use LookPathAbs by default (assuming it is added)
The problem with #38736 is that it silently changes the behavior of programs on Windows. Where exec.Command("prog") previously found and ran .\prog.exe, it would now either silently switch to a prog.exe from the PATH (surprise!) or return an error that prog could not be found (even though it used to be; confusion!). The same is true of exec.LookPath.
#42420 avoids the problem of changing existing behavior by introducing a new function exec.LookPathAbs that never looks in dot. Clearly that doesn’t surprise or confuse anyone. But it also doesn’t fix any security problems.
#42950 extends #42420 by changing exec.Command to use exec.LookPathAbs by default. That brings back the surprise and confusion of #38736, but only for exec.Command and not exec.LookPath. And compared to #38736, #42420+#42950 has the added complexity of adding new API (LookPathAbs).
None of these are great.
The proposal in this issue, to adopt execabs semantics as the os/exec semantics, fixes the problems. Execabs doesn’t remove dot from the PATH lookup. Instead it reports use of dot as an error. This avoids the surprise of running a different program. And the reported error is very clear about what happened. Instead of a generic “program not found” it gives an error that avoids the confusion:
prog resolves to executable in current directory (./prog)
And of course because programs from the current directory are not being executed, that fixes the security problem too.
The specific changes this issue proposes in os/exec are:
- Add a new
var ErrDot = errors.New("executable in current directory") - Change
LookPath: if it would have chosen toreturn path, nilwhere path is an executable in the current directory found by a PATH lookup, it now doesreturn path, errwhereerrsatisfieserrors.Is(err, ErrDot). - Change
Cmd: add a new fieldErr errorwhich is returned byStartorRunif not set. This field replaces the current unexported fieldlookPathErr.
Consider this client code:
path, err := exec.LookPath("prog")
if err != nil {
log.Fatal(err)
}
use(path)
cmd := exec.Command("prog")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
With the proposed changes, the code would no longer find and run ./prog on Unix (when dot is in the PATH), nor .\prog.exe on Windows (regardless of PATH), addressing the security issue.
Programs that want to require the current directory to be used (ignoring PATH) can change "prog" to "./prog" (that works on both Unix and Windows systems). This change ("prog" to "./prog") is compatible with older versions of os/exec.
Programs that want to allow the use of the current directory in conjunction with PATH can add a few new lines of code, marked with <<<:
path, err := exec.LookPath("prog")
if errors.Is(err, exec.ErrDot) { // <<<
err = nil // <<<
} // <<<
if err != nil {
log.Fatal(err)
}
use(path)
cmd := exec.Command("prog")
if errors.Is(cmd.Err, exec.ErrDot) { // <<<
cmd.Err = nil // <<<
} // <<<
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
This should give programs the flexibility to opt back into the old behavior when necessary.
Of course, this change (adding the errors.Is checks) is not compatible with older versions of os/exec, but we expect this need to be rare. We expect most programs that intentionally run programs from the current directory to update to the ./prog form.
Windows users, would this proposal break your programs?
You can check today by replacing
import "os/exec"
with
import exec "golang.org/x/sys/execabs"
in your source code. No other changes are needed to get the effect. (Of course, golang.org/x/sys/execabs does not provide ErrDot, so the errors.Is stanzas cannot be written in this simulation of the proposal.)
Thoughts? Comments? Concerns? Thanks very much.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 44
- Comments: 53 (21 by maintainers)
Commits related to this issue
- cmd/coordinator: use explicit ./ in exec to say we want a relative exec This makes cmd/coordinator continue to work even once os/exec starts rejecting such things as mistakes by default. Fixes the f... — committed to golang/build by rsc 2 years ago
- Revert "os/exec: return error when PATH lookup would use current directory" This reverts CL 381374. Reason for revert: broke tests for x/sys/execabs. Updates #43724. Updates #43947. Change-Id: I9e... — committed to golang/go by bcmills 2 years ago
- execabs: make safe for Go 1.19 To preserve the same errors that LookPath used to return, LookPath needs to know to ignore the new Go 1.19 exec.ErrDot errors. For golang/go#43724. Change-Id: I893881... — committed to golang/sys by rsc 2 years ago
- os/exec: return error when PATH lookup would use current directory CL 381374 was reverted because x/sys/execabs broke. This CL reapplies CL 381374, but adding a lookPathErr error field back, for exe... — committed to golang/go by rsc 2 years ago
- all: use os/exec instead of internal/execabs We added internal/execabs back in January 2021 in order to fix a security problem caused by os/exec's handling of the current directory. Now that os/exec ... — committed to golang/go by rsc 2 years ago
- os/exec: in Command, update cmd.Path even if LookPath returns an error Fixes #52666. Updates #43724. Updates #43947. Change-Id: I72cb585036b7e93cd7adbff318b400586ea97bd5 Reviewed-on: https://go-revi... — committed to golang/go by bcmills 2 years ago
- Use absolute path to shims dir in docker when tools are econfigured. It's important that we use the absolute path to `.shims/bin` because the use of a relative path raises an error. The error comes f... — committed to danxmoran/pants by danxmoran 2 years ago
- os/exec: return clear error for missing cmd.Path Following up on CL 403694, there is a bit of confusion about when Path is and isn't set, along with now the exported Err field. Catch the case where P... — committed to golang/go by rsc 2 years ago
- [release-branch.go1.18] os/exec: return clear error for missing cmd.Path Following up on CL 403694, there is a bit of confusion about when Path is and isn't set, along with now the exported Err field... — committed to golang/go by rsc 2 years ago
- [release-branch.go1.17] os/exec: return clear error for missing cmd.Path Following up on CL 403694, there is a bit of confusion about when Path is and isn't set, along with now the exported Err field... — committed to golang/go by rsc 2 years ago
- os/exec: on Windows, suppress ErrDot if the implicit path matches the explicit one If the current directory is also listed explicitly in %PATH%, this changes the behavior of LookPath to prefer the ex... — committed to golang/go by bcmills 2 years ago
- os/exec: add GODEBUG setting to opt out of ErrDot changes The changes are likely to break users, and we need to make it easy to unbreak without code changes. For #43724. Fixes #53962. Change-Id: I1... — committed to golang/go by rsc 2 years ago
- os/exec: on Windows, suppress ErrDot if the implicit path matches the explicit one If the current directory is also listed explicitly in %PATH%, this changes the behavior of LookPath to prefer the ex... — committed to jproberts/go by bcmills 2 years ago
- os/exec: add GODEBUG setting to opt out of ErrDot changes The changes are likely to break users, and we need to make it easy to unbreak without code changes. For #43724. Fixes #53962. Change-Id: I1... — committed to jproberts/go by rsc 2 years ago
- Always prepend caddy command with dot (#117) Fix xcaddy run on windows with go 1.19 See https://github.com/golang/go/issues/43724 — committed to caddyserver/xcaddy by ydylla 2 years ago
Except for the discussion around go generate, the reaction here seems overwhelmingly in favor, and this is the kind of change we should land early in the cycle. It sounds like the consensus is to accept this proposal.
Do I have that right?
Thank you for the detailed description, @rsc!
If this proposal would be adopted, how should we write the following code so it would be safe to run inside an untrusted directory?
With this proposal, if the current directory has a
.\git.exeor a.\git.bat(or a similar file matching any of the PATHEXTs), running this command would result in a failure. Since there might be legitimate reasons why the current directory could contain agit.bat, would we have any option to instead execute git as found in the system PATH instead of in the current directory?I understand that this proposal is meant to prevent silent failures in programs that relied on the Windows behavior. I’m just wondering, if this proposal gets accepted, what are our options for running commands in untrusted directories (i.e. so that the contents of the current directory is entirely ignored)? Might this proposal ship in tandem with https://github.com/golang/go/issues/42420?
@dylan-bourque wouldn’t using the absolute path to the bin directory be a more proper fix for your situation?
Right. That snippet did feel off. I’ve never been a fan of altering global state and it doesn’t get much more global than the environment. Somehow I thought it would make sense to “put things back the way they were”, but, yeah, it’s not like anyone else really cares what a process does to its own environment, apart from the children that process may spawn.
My primary concern here is that it’s possible to interfere with an application that depends on stuff off the
PATHjust by altering it’s working directory: trick it into launching “fun” surprises or make it error out (respectively, before and after the implementation of this proposal). I wouldn’t want either of those happening.I used to put a script at the project root sometimes, in my PHP days, naming it
phpunit, just like the real executable, to invoke PHPUnit simply as./phpunitinstead of./vendor/bin/phpunit. Yes, I’m that lazy. Sometimes I added some CLI parameters to that script, if a common set could be distilled, to have less to type. The problem now is that such a script in a current working directory would prevent a tool written in Go from intentionally starting the PHPUnit (in this example) from thePATH, unlessNoDefaultCurrentDirectoryInExePathis set. Or it could be Git. Or Docker. Or whatever other executable expected to be found on thePATH. Would that be a problem on Windows systems only? Would others be safe from such a denial of service condition, or does it require a more extensive workaround to be reliably mitigated cross-platform?Asking because not only am I lazy, I’m also selfish, smug and opinionated — I don’t use Windows and I think nobody else ever should either. If it’s only on that OS that Go programs break this way, well, stuff breaks on that OS all the time, and those who still use it regardless ought to have had resigned to that fate by now. It’s not like there is a lack of choice of alternatives. So, yeah, if it’s only that OS that goes bork yet again now, then, meh, fine, whatever, they had it coming to them.
If I specifically wanted command execution to proceed exhibiting weird quirks characteristic of a single specific platform (https://github.com/golang/go/issues/43947#issuecomment-780877042 lists steps that should be taken to locate and start an executable precisely the way that OS does), I’d look for that in a specific platform compatibility layer. Not in a supposedly sane cross-platform standard library API.
This is done now.
RE: the break due to
lookupPathErr, it’s likely the case that there’s code out there that’s going to be using old versions ofexecabsfor a while (e.g. stable versions of gopls, other libraries that were bulk updated, most of all users like me who took the advice of the blog posts and released code with that dependency).I think
execabsshould be changed to a no-op on new builds, yes, but will a fakelookupPathErrbe left behind to ensure that code built with newer toolchains continues to work? Since newer versions of Go would not do the relative lookup, I would think that oldexecabstouching an unexported field (but doing nothing) would maintain compatibility.No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group
Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group
My only outstanding concern would be the inability to ignore the current directory in the path search, as mentioned in previous comments. The primary security issue is addressed by avoiding a potentially malicious executable in the current directory, but possibly transformed into a less severe denial-of-service. Rather than causing execution of malicious code, a hypothetical attacker’s rogue file will now prevent the Go-implemented service from performing its function. However, your proposal to separately respect the NoDefaultCurrentDirectoryInExePath environment variable would overcome that.
It might be convenient to avoid the new error if dot is explicitly present in PATH, though one could certainly achieve that via some additional application code in conjunction with errors.Is(err, exec.ErrDot). I don’t think that I foresee having such a use case in my own work, and the new scenario will be that the normally discouraged/“bad” practice is what takes additional effort to enable.
@bcmills, cmd.Err is not strictly necessary - users can call LookPath themselves - but it is error-prone to assume the result of LookPath will be valid input to exec.Command. In general that’s not true, since LookPath returns a clean path, so
gonot./go. That is,absin your code snippet is not always an absolute path. It’s far easier and less error-prone to apply the check after the exec.Command lookup instead of keeping two lookups in sync.Also, there are now two packages (execabs and safeexec) that function as exec.Command-workalikes and are hampered in that by not being able to set Err themselves. Safeexec uses a not-as-nice API and execabs uses ugly reflection. Better to expose Err and let any future wrappers do what they need to do. That field (lookPathErr) is the only setup field in exec.Command that’s not exported.
@dolmen, yes, there is no “do a non-standard PATH lookup”. There is only “do the standard PATH lookup and refuse an insecure result”. As noted above, the problem with “do a non-standard PATH lookup” is that it silently differs from the standard system behavior, which will cause mystery and confusion.
Those same users really probably should delete go.bat. If your goeval doesn’t trip over it, something else will.
I think it would be fine, as an independent change, to respect the presence of the NoDefaultCurrentDirectoryInExePath environment variable. If that variable were found in the environment, then LookPath would never look in dot and the security check would never trigger.
@rsc This proposal helps for the case where you want to catch the case of a local executable file. But it doesn’t help for the case where you want to
LookPathignoring a local executable and looking just in %PATH%: this proposal doesn’t provide aLookPathIgnoreDotand still forces to use an alternate version of the wholeos/execpackage.Concrete example: In my
goevalproject I want to rungo runto compile a Go source file.golang.org/x/sys/execabscatches a security issue if there is ago.batin the current directory, but that’s not what is helpful for users. I need instead anexec.Commandthat lookupgoonly from%PATH%. I just want the Unix behavior (which Windows user can opt-in on non-Go programs because ofNeedCurrentDirectoryForExePathW).Update: It looks like my only option is to vendor both
os/execandgolang.org/x/sys/execabsin my project (with the patched version ofLookPathI want) to get the behavior I need.Update 2:
Do I also have to vendor that
internal/execabs?For some additional context, I recently updated to Go 1.15.7 (which includes the security patch for this issue) and it broke build-time scripts that run on Mac and/or Linux systems. We have a build process where we intentionally
go installbuild tools to<project-dir>/bin(in order to avoid installing local tools to “global” directories that happen to appear in$PATH) and prepend<project-dir>/binto$PATH. We then have//go:generate custom-tool ....directives in .go source files within the project, with the obvious expectation that it will find thecustom-toolbinary that we just “installed” into<project-dir>/bin.Unfortunately for me, running
go generate ./...from the project root now fails with an error that says:The suggested workaround is to prepend
./(or./bin/in my case) would work but it now makes thosego:generatedirectives sensitive to which directorygo generateis run from, i.e. ago:generatedirective in a sub-package would work whengo generateis run from the project root but fail if the command was run from the sub-package directory.While I fully understand the security concern here, I take issue with the premise that all situations that involve executing a binary that resides within the current directory are invalid. With this change to Go, I now have approximately 800
go:generatedirectives that I need to investigate to see whether or not they are broken.