Go version
go version go1.25rc1 darwin/arm64 go version go1.24.4 darwin/arm64 go version go1.24.4 linux/amd64 go version go1.24.4 windows/amd64 ...
What did you do?
The os/exec.LookPath
function incorrectly resolves the given path without raising an error, when both of the following circumstances are matched:
- the
PATH
environment variable contains one element that is a path to an executable file that is not a directory - the path given to
exec.LookPath
, when joined to the path above usingfilepath.Join()
, gives a path to the executable file inPATH
. Such values include""
(the empty string) or"."
(dot)
In that case, the returned error is nil
(no error) and the resolved path is the path to the executable file in PATH
.
On Windows the incorrect resolution of ""
and "."
can also be triggered when a malicious executable file exists in the parent directory of an element of PATH
with the name of that PATH element and a PATHEXT
extension. For example, exec.LookPath("")
resolves to "C:\utils\bin.cmd"
if "C:\utils\bin"
is in PATH
and "C:\utils\bin.cmd"
exists. In that case the control of the value of the PATH
environment variable is not necessary for the exploitation.
As the output of a successful call to exec.LookPath
is usually given to exec.Command
, this bug could be used to trigger the execution of the malicious PATH
element as the command, while that is not obvious when looking at the calling code. Cmd.Start
contains protections against running an empty command, but here we are in a case where LookPath
transforms an empty path into a non-empty path pointing to a real executable file, and so allows to bypass that protection.
Proof of concept
The full code of the program below is available on the Go Playground: https://go.dev/play/p/scC3h8jGPuz
func check(s string) {
fmt.Println("PATH:", os.Getenv("PATH"))
fmt.Printf("exec.LookPath(%q)\n", s)
p, err := exec.LookPath(s)
fmt.Printf("-> err: %v\n-> p: %q\n", err, p)
}
func main() {
check("")
check(".")
// Let's add an executable file in PATH
os.Setenv("PATH", os.Getenv("PATH")+string(filepath.ListSeparator)+os.Args[0])
check("")
check(".")
}
Output:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
exec.LookPath("")
-> err: exec: "": executable file not found in $PATH
-> p: ""
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
exec.LookPath(".")
-> err: exec: ".": executable file not found in $PATH
-> p: ""
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/tmpfs/play
exec.LookPath("")
-> err: <nil>
-> p: "/tmpfs/play"
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/tmpfs/play
exec.LookPath(".")
-> err: <nil>
-> p: "/tmpfs/play"
Here is some example of exploiting the issue to make exec.Cmd.Run
execute the program injected in $PATH
instead of failing because the LookPath
argument is empty.
That program runs on the Go Playground: https://go.dev/play/p/yivKGIeemiB
func main() {
// Binary injected in PATH. This could happen outside the program
os.Setenv("PATH", "/bin/date")
// Vulnerable code that executes /bin/date instead of failing because to the empty command
p, err := exec.LookPath("")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command(p)
cmd.Stdout = os.Stdout
cmd.Run()
}
Output:
Mon Jun 23 21:21:10 UTC 2025
Severity
So far I'm not considering this bug to be a real risk in the general case as any real exploit requires the control of both PATH
and the argument to exec.LookPath
, or on Windows a writable directory parent to a PATH
element. An attacker who has just control of PATH
already has a wide range of attacks possible, so this bug doesn't increase the vulnerability of the system.
However, this bug could still be used to hide the launch of an external command to a code reviewer or to static analysis tools. The path to a malicious binary could be added to PATH
in some place and the path given to os/exec.LookPath
could be just left uninitialized in some obscure path or from user input, and the execution of the binary specified in PATH would be triggered.
Also, as a Go developer, I was trusting exec.LookPath
to sanitize user input before calling exec.Command
. This bug breaks this expectation (a command name that would be rejected by Cmd.Start
like ""
may still lead to an execution).
Notes
Following the Go Security Policy this bug has been privately reported to the Go security team on 2025-06-24 who qualified it as a PUBLIC track issue.
Comment From: gabyhelp
Related Issues
- proposal: os/exec: make LookPath not look in dot implicitly on Windows #38736 (closed)
- os/exec: return error when PATH lookup would use current directory #43724 (closed)
- os.exec and file path resolving #73910 (closed)
- os/exec: unexpected ErrDot returned if there is an empty path in PATH #61493 (closed)
- proposal: os/exec: add LookPathAbs that refuses to return relative paths #42420 (closed)
- os/exec: LookPath() doesn't consider chroot #39341
- os/exec: exec.LookPath return errors when PATH starts with semicolon (parsed as contains empty string at first) and working directory is same as the target executable on windows #69636 (closed)
- os/exec: windows LookPath is broken #6224 (closed)
- os/exec: Command does a LookPath in the wrong place. #7228 (closed)
- os/exec: calling Cmd.Start after setting Cmd.Path manually to absolute path without ".exe" no longer implicitly adds ".exe" in Go 1.22 #66586 (closed)
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: gopherbot
Change https://go.dev/cl/685755 mentions this issue: os/exec: fix incorrect expansion of "" and "." in LookPath
Comment From: dolmen
CL685755 is my proposal for a fix. It doesn't handle Plan 9 (that OS has its own implmentation of LookPath in lp_plan9.go).
The approach in this patch is to rule out early in LookPath the "" and "." values.
I have also considered some other approaches: * check if filepath.Join(pathElement, input) had an unexpected result such as being too similar to pathElement. But that check would be more complex to implement (also more costly at runtime) and might be a path open for more quirks. Also, the check would be in the loop for pathElement while my more simple check isn't. * skip any pathElement that is not a directory. That would add a call to stat(2) that is also more costly than my selected solution.
On Windows my solution rules out resolving "" as ".exe" or "." as "..cmd". I've not yet checked the existing behavior (I haven't yet run the testsuite on Windows), to see if this solutions breaks compatibility. I would appreciate early feedback before investing more time in portability issues.