From 751092c8efbd2bba912e95c04bfa35d8883fe61b Mon Sep 17 00:00:00 2001 From: hhang <17309584214@163.com> Date: Sat, 13 Jun 2026 18:56:13 +0800 Subject: [PATCH] fix(vfs): reject Windows absolute paths cross-platform (#1401) * fix(vfs): reject Windows absolute paths cross-platform * test(vfs): cover input Windows absolute paths --- internal/vfs/localfileio/path.go | 21 ++++++++++++++++++--- internal/vfs/localfileio/path_test.go | 26 +++++++++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/internal/vfs/localfileio/path.go b/internal/vfs/localfileio/path.go index 7a4b52ab..d0209737 100644 --- a/internal/vfs/localfileio/path.go +++ b/internal/vfs/localfileio/path.go @@ -60,12 +60,12 @@ func safePath(raw, flagName string) (string, error) { return "", err } - path := filepath.Clean(raw) - - if filepath.IsAbs(path) { + if isAbsolutePath(raw) { return "", fmt.Errorf("%s must be a relative path within the current directory, got %q (hint: cd to the target directory first, or use a relative path like ./filename)", flagName, raw) } + path := filepath.Clean(raw) + cwd, err := vfs.Getwd() if err != nil { return "", fmt.Errorf("cannot determine working directory: %w", err) @@ -114,6 +114,21 @@ func resolveNearestAncestor(path string) (string, error) { } } +func isAbsolutePath(path string) bool { + path = strings.TrimSpace(path) + if path == "" { + return false + } + if filepath.IsAbs(path) || strings.HasPrefix(path, "/") || strings.HasPrefix(path, `\`) { + return true + } + if len(path) >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') { + drive := path[0] + return ('A' <= drive && drive <= 'Z') || ('a' <= drive && drive <= 'z') + } + return false +} + func isUnderDir(child, parent string) bool { rel, err := filepath.Rel(parent, child) if err != nil { diff --git a/internal/vfs/localfileio/path_test.go b/internal/vfs/localfileio/path_test.go index 946d1be1..3cfb8e74 100644 --- a/internal/vfs/localfileio/path_test.go +++ b/internal/vfs/localfileio/path_test.go @@ -34,6 +34,10 @@ func TestSafeOutputPath_RejectsPathTraversalAndDangerousInput(t *testing.T) { // ── GIVEN: absolute paths → THEN: rejected ── {"absolute path unix", "/etc/passwd", true}, {"absolute path root", "/tmp/evil", true}, + {"absolute path windows drive", `C:\Users\agent\secret.txt`, true}, + {"absolute path windows drive slash", "C:/Users/agent/secret.txt", true}, + {"absolute path windows rooted", `\Users\agent\secret.txt`, true}, + {"absolute path windows unc", `\\server\share\secret.txt`, true}, // ── GIVEN: control characters in path → THEN: rejected ── {"null byte", "file\x00.txt", true}, @@ -187,11 +191,23 @@ func TestSafeUploadPath_AllowsTempFileAbsolutePath(t *testing.T) { } func TestSafeUploadPath_RejectsNonTempAbsolutePath(t *testing.T) { - // GIVEN: an absolute path outside the temp directory - // WHEN / THEN: SafeUploadPath rejects it - _, err := SafeInputPath("/etc/passwd") - if err == nil { - t.Error("expected error for absolute non-temp path, got nil") + for _, tt := range []struct { + name string + input string + }{ + {"absolute path unix", "/etc/passwd"}, + {"absolute path windows drive", `C:\Users\agent\secret.txt`}, + {"absolute path windows drive slash", "C:/Users/agent/secret.txt"}, + {"absolute path windows rooted", `\Users\agent\secret.txt`}, + {"absolute path windows unc", `\\server\share\secret.txt`}, + } { + t.Run(tt.name, func(t *testing.T) { + // WHEN / THEN: SafeInputPath rejects absolute paths on every platform. + _, err := SafeInputPath(tt.input) + if err == nil { + t.Errorf("expected error for absolute path %q, got nil", tt.input) + } + }) } }