src/os/exec/dot_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/os/exec/exec.go | 10 ++++++++++ src/os/exec/lp_plan9.go | 4 ++++ src/os/exec/lp_unix.go | 4 ++++ src/os/exec/lp_windows.go | 8 ++++++++ diff --git a/src/os/exec/dot_test.go b/src/os/exec/dot_test.go index 1bf0d9c760bfd04e0caec444994bcf04a75d52d1..b95639e6c82068e0af09f2801eae39d01e56cecf 100644 --- a/src/os/exec/dot_test.go +++ b/src/os/exec/dot_test.go @@ -177,4 +177,48 @@ t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, nil`, "execabs-test", found, err, wantFound) } } }) + + checker := func(test string) func(t *testing.T) { + return func(t *testing.T) { + t.Helper() + t.Logf("PATH=%s", os.Getenv("PATH")) + p, err := LookPath(test) + if err == nil { + t.Errorf("%q: error expected, got nil", test) + } + if p != "" { + t.Errorf("%q: path returned should be \"\". Got %q", test, p) + } + } + } + + // Reference behavior for the next test + t.Run(pathVar+"=$OTHER2", func(t *testing.T) { + t.Run("empty", checker("")) + t.Run("dot", checker(".")) + t.Run("dotdot1", checker("abc/..")) + t.Run("dotdot2", checker("..")) + }) + + // Test the behavior when PATH contains an executable file which is not a directory + t.Run(pathVar+"=exe", func(t *testing.T) { + // Inject an executable file (not a directory) in PATH. + // Use our own binary os.Args[0]. + t.Setenv(pathVar, testenv.Executable(t)) + t.Run("empty", checker("")) + t.Run("dot", checker(".")) + t.Run("dotdot1", checker("abc/..")) + t.Run("dotdot2", checker("..")) + }) + + // Test the behavior when PATH contains an executable file which is not a directory + t.Run(pathVar+"=exe/xx", func(t *testing.T) { + // Inject an executable file (not a directory) in PATH. + // Use our own binary os.Args[0]. + t.Setenv(pathVar, filepath.Join(testenv.Executable(t), "xx")) + t.Run("empty", checker("")) + t.Run("dot", checker(".")) + t.Run("dotdot1", checker("abc/..")) + t.Run("dotdot2", checker("..")) + }) } diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go index 91a6831b04f4283e1b7580bdf8f9e350ff7a2341..38354a5244322ffebdb4cb24c112d9b4e11fb2d0 100644 --- a/src/os/exec/exec.go +++ b/src/os/exec/exec.go @@ -1328,3 +1328,13 @@ // Note that functions in this package do not return ErrDot directly. // Code should use errors.Is(err, ErrDot), not err == ErrDot, // to test whether a returned error err is due to this condition. var ErrDot = errors.New("cannot run executable found relative to current directory") + +// validateLookPath excludes paths that can't be valid +// executable names. See issue #74466 and CVE-2025-47906. +func validateLookPath(s string) error { + switch s { + case "", ".", "..": + return ErrNotFound + } + return nil +} diff --git a/src/os/exec/lp_plan9.go b/src/os/exec/lp_plan9.go index 87359b3551d32f0d5ec717e1c9758b03974ccce9..0430af9eefeb420adcb9788b96ed134dc6f7d36a 100644 --- a/src/os/exec/lp_plan9.go +++ b/src/os/exec/lp_plan9.go @@ -36,6 +36,10 @@ // In older versions of Go, LookPath could return a path relative to the current directory. // As of Go 1.19, LookPath will instead return that path along with an error satisfying // [errors.Is](err, [ErrDot]). See the package documentation for more details. func LookPath(file string) (string, error) { + if err := validateLookPath(file); err != nil { + return "", &Error{file, err} + } + // skip the path lookup for these prefixes skip := []string{"/", "#", "./", "../"} diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go index 8617d45e983e6ee1f221676ba34b9f6cf44b7d02..e5fddbafe21b94e4597b45a565a021137014165a 100644 --- a/src/os/exec/lp_unix.go +++ b/src/os/exec/lp_unix.go @@ -54,6 +54,10 @@ // NOTE(rsc): I wish we could use the Plan 9 behavior here // (only bypass the path if file begins with / or ./ or ../) // but that would not match all the Unix shells. + if err := validateLookPath(file); err != nil { + return "", &Error{file, err} + } + if strings.Contains(file, "/") { err := findExecutable(file) if err == nil { diff --git a/src/os/exec/lp_windows.go b/src/os/exec/lp_windows.go index 1225674306458509ae694a9612e109dcd1730ae6..e01e7bbbbabde08eb9b4dbeae613dd40727d966a 100644 --- a/src/os/exec/lp_windows.go +++ b/src/os/exec/lp_windows.go @@ -67,6 +67,10 @@ // In older versions of Go, LookPath could return a path relative to the current directory. // As of Go 1.19, LookPath will instead return that path along with an error satisfying // [errors.Is](err, [ErrDot]). See the package documentation for more details. func LookPath(file string) (string, error) { + if err := validateLookPath(file); err != nil { + return "", &Error{file, err} + } + return lookPath(file, pathExt()) } @@ -80,6 +84,10 @@ // for additional extensions. For example, // "C:\foo\example.com" would be returned as-is even if the // program is actually "C:\foo\example.com.exe". func lookExtensions(path, dir string) (string, error) { + if err := validateLookPath(path); err != nil { + return "", &Error{path, err} + } + if filepath.Base(path) == path { path = "." + string(filepath.Separator) + path }