From 2f8b9cbdfb7ab331e8066ae04e5fbda43ebfa827 Mon Sep 17 00:00:00 2001 From: Aditya Menon Date: Thu, 12 Feb 2026 12:22:51 +0530 Subject: [PATCH] fix: prevent local absolute paths from being treated as remote URLs (#2397) Add filepath.IsAbs guard to IsRemote and Parse to prevent Windows drive letter paths (e.g., C:\path) from being misinterpreted as remote URLs by go-getter's url.Parse, which prepends file:// to drive letter paths. Fixes #2384 Signed-off-by: Aditya Menon --- pkg/remote/remote.go | 10 +++++ pkg/remote/remote_test.go | 53 ++++++++++++++++++++++++ pkg/remote/remote_windows_test.go | 69 +++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 pkg/remote/remote_windows_test.go diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 4bc11e4a..01430b52 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -102,6 +102,9 @@ type Source struct { } func IsRemote(goGetterSrc string) bool { + if filepath.IsAbs(goGetterSrc) { + return false + } if _, err := Parse(goGetterSrc); err != nil { return false } @@ -122,6 +125,13 @@ func Parse(goGetterSrc string) (*Source, error) { } } + // Local absolute paths (e.g., C:\path on Windows, /path on Unix) + // are not remote URLs. Reject them before go-getter's url.Parse + // can misinterpret Windows drive letters as URL schemes. + if filepath.IsAbs(goGetterSrc) { + return nil, InvalidURLError{err: fmt.Sprintf("parse url: local absolute path is not a remote URL: %s", goGetterSrc)} + } + u, err := url.Parse(goGetterSrc) if err != nil { return nil, InvalidURLError{err: fmt.Sprintf("parse url: %v", err)} diff --git a/pkg/remote/remote_test.go b/pkg/remote/remote_test.go index cfee6086..88ddf428 100644 --- a/pkg/remote/remote_test.go +++ b/pkg/remote/remote_test.go @@ -356,6 +356,54 @@ func TestRemote_S3(t *testing.T) { } } +func TestIsRemote(t *testing.T) { + testcases := []struct { + name string + input string + expected bool + }{ + { + name: "git remote URL", + input: "git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0", + expected: true, + }, + { + name: "s3 remote URL", + input: "s3://helm-s3-values-example/values.yaml", + expected: true, + }, + { + name: "https remote URL", + input: "https://example.com/values.yaml", + expected: true, + }, + { + name: "relative path", + input: "relative/path/to/file.yaml", + expected: false, + }, + { + name: "parent-relative path", + input: "../services/values.yaml", + expected: false, + }, + { + name: "unix absolute path", + input: "/absolute/path/to/file.yaml", + expected: false, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + result := IsRemote(tt.input) + if result != tt.expected { + t.Errorf("IsRemote(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + func TestParse(t *testing.T) { testcases := []struct { name string @@ -372,6 +420,11 @@ func TestParse(t *testing.T) { input: "raw/incubator", err: "parse url: missing scheme - probably this is a local file path? raw/incubator", }, + { + name: "unix absolute path", + input: "/absolute/path/to/file.yaml", + err: "parse url: local absolute path is not a remote URL: /absolute/path/to/file.yaml", + }, { name: "git scheme", input: "git::https://github.com/stakater/Forecastle.git@deployments/kubernetes/chart/forecastle?ref=v1.0.54", diff --git a/pkg/remote/remote_windows_test.go b/pkg/remote/remote_windows_test.go new file mode 100644 index 00000000..6f25c027 --- /dev/null +++ b/pkg/remote/remote_windows_test.go @@ -0,0 +1,69 @@ +//go:build windows + +package remote + +import ( + "testing" +) + +func TestIsRemote_Windows(t *testing.T) { + testcases := []struct { + name string + input string + expected bool + }{ + { + name: "windows drive letter path", + input: `C:\project\services\values.yaml`, + expected: false, + }, + { + name: "windows UNC path", + input: `\\server\share\path\values.yaml`, + expected: false, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + result := IsRemote(tt.input) + if result != tt.expected { + t.Errorf("IsRemote(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestParse_Windows(t *testing.T) { + testcases := []struct { + name string + input string + err string + }{ + { + name: "windows drive letter path", + input: `C:\project\services\values.yaml`, + err: `parse url: local absolute path is not a remote URL: C:\project\services\values.yaml`, + }, + { + name: "windows UNC path", + input: `\\server\share\path\values.yaml`, + err: `parse url: local absolute path is not a remote URL: \\server\share\path\values.yaml`, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.input) + if err == nil { + t.Fatalf("Parse(%q) expected error, got nil", tt.input) + } + if _, ok := err.(InvalidURLError); !ok { + t.Fatalf("Parse(%q) expected InvalidURLError, got %T: %v", tt.input, err, err) + } + if err.Error() != tt.err { + t.Errorf("Parse(%q) error = %q, want %q", tt.input, err.Error(), tt.err) + } + }) + } +}