From 8fa31a2d7d9e60c50a3a94080c097b6e65773f4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= <olivier.mengue@gmail.com>
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
(cherry picked from commit e0b07dc)
Reviewed-on: https://go-review.googlesource.com/c/go/+/691855
Reviewed-by: Michael Knyszek <mknyszek@google.com>

CVE: CVE-2025-47906

Upstream-Status: Backport [https://github.com/golang/go/commit/8fa31a2d7d9e60c50a3a94080c097b6e65773f4b]

Signed-off-by: Archana Polampalli <archana.polampalli@windriver.com>
---
 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
