From 8fa31a2d7d9e60c50a3a94080c097b6e65773f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Mon, 30 Jun 2025 16:58:59 +0200 Subject: [PATCH] [release-branch.go1.23] os/exec: fix incorrect expansion of "", "." and ".." in LookPath Fix incorrect expansion of "" and "." when $PATH contains an executable file or, on Windows, a parent directory of a %PATH% element contains an file with the same name as the %PATH% element but with one of the %PATHEXT% extension (ex: C:\utils\bin is in PATH, and C:\utils\bin.exe exists). Fix incorrect expansion of ".." when $PATH contains an element which is an the concatenation of the path to an executable file (or on Windows a path that can be expanded to an executable by appending a %PATHEXT% extension), a path separator and a name. "", "." and ".." are now rejected early with ErrNotFound. Fixes CVE-2025-47906 Fixes #74803 Change-Id: Ie50cc0a660fce8fbdc952a7f2e05c36062dcb50e Reviewed-on: https://go-review.googlesource.com/c/go/+/685755 LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Reviewed-by: Roland Shoemaker Reviewed-by: Damien Neil (cherry picked from commit e0b07dc) Reviewed-on: https://go-review.googlesource.com/c/go/+/691855 Reviewed-by: Michael Knyszek CVE: CVE-2025-47906 Upstream-Status: Backport [https://github.com/golang/go/commit/8fa31a2d7d9e60c50a3a94080c097b6e65773f4b] Signed-off-by: Archana Polampalli --- src/internal/execabs/execabs_test.go | 55 ++++++++++++++++++++++++++++ src/os/exec/exec.go | 9 +++++ src/os/exec/lp_plan9.go | 4 ++ src/os/exec/lp_unix.go | 4 ++ src/os/exec/lp_windows.go | 4 ++ 5 files changed, 76 insertions(+) diff --git a/src/internal/execabs/execabs_test.go b/src/internal/execabs/execabs_test.go index 97a3f39..99fd64b 100644 --- a/src/internal/execabs/execabs_test.go +++ b/src/internal/execabs/execabs_test.go @@ -100,4 +100,59 @@ func TestLookPath(t *testing.T) { } else if err.Error() != expectedErr { t.Errorf("LookPath returned unexpected error: want %q, got %q", expectedErr, err.Error()) } + 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]. + testenv.MustHaveExec(t) + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + t.Setenv(pathVar, exe) + 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]. + testenv.MustHaveExec(t) + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + t.Setenv(pathVar, filepath.Join(exe, "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 505de58..84fd82f 100644 --- a/src/os/exec/exec.go +++ b/src/os/exec/exec.go @@ -790,3 +790,12 @@ func addCriticalEnv(env []string) []string { } return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) } +// 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 e8826a5..ed9f6e3 100644 --- a/src/os/exec/lp_plan9.go +++ b/src/os/exec/lp_plan9.go @@ -33,6 +33,10 @@ func findExecutable(file string) error { // The result may be an absolute path or a path relative to the current directory. func LookPath(file string) (string, error) { // skip the path lookup for these prefixes + if err := validateLookPath(file); err != nil { + return "", &Error{file, err} + } + skip := []string{"/", "#", "./", "../"} for _, p := range skip { diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go index d1d246a..1b27f2b 100644 --- a/src/os/exec/lp_unix.go +++ b/src/os/exec/lp_unix.go @@ -38,6 +38,10 @@ func LookPath(file string) (string, error) { // (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 e7a2cdf..7a1d6fb 100644 --- a/src/os/exec/lp_windows.go +++ b/src/os/exec/lp_windows.go @@ -58,6 +58,10 @@ func findExecutable(file string, exts []string) (string, error) { // a suitable candidate. // The result may be an absolute path or a path relative to the current directory. func LookPath(file string) (string, error) { + if err := validateLookPath(file); err != nil { + return "", &Error{file, err} + } + var exts []string x := os.Getenv(`PATHEXT`) if x != "" { -- 2.40.0