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 using filepath.Join(), gives a path to the executable file in PATH. 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

(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.