diff --git a/agentci/security.go b/agentci/security.go index 2a0f777..1898527 100644 --- a/agentci/security.go +++ b/agentci/security.go @@ -16,28 +16,36 @@ import ( var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`) // SanitizePath ensures a filename or directory name is safe and prevents path traversal. -// Returns the validated input unchanged. +// Returns the validated basename. // Usage: SanitizePath(...) func SanitizePath(input string) (string, error) { if input == "" { return "", coreerr.E("agentci.SanitizePath", "path element is required", nil) } - if strings.ContainsAny(input, `/\`) { - return "", coreerr.E("agentci.SanitizePath", "path separators are not allowed: "+input, nil) - } - if input == "." || input == ".." { + safeName := filepath.Base(input) + if safeName == "." || safeName == ".." { return "", coreerr.E("agentci.SanitizePath", "invalid path element: "+input, nil) } - if !safeNameRegex.MatchString(input) { + if strings.ContainsAny(safeName, `/\`) { + return "", coreerr.E("agentci.SanitizePath", "path separators are not allowed: "+input, nil) + } + if !safeNameRegex.MatchString(safeName) { return "", coreerr.E("agentci.SanitizePath", "invalid characters in path element: "+input, nil) } - return input, nil + return safeName, nil } // ValidatePathElement validates a single local path element and returns its safe form. // Usage: ValidatePathElement(...) func ValidatePathElement(input string) (string, error) { - return SanitizePath(input) + safeName, err := SanitizePath(input) + if err != nil { + return "", err + } + if safeName != input { + return "", coreerr.E("agentci.ValidatePathElement", "path separators are not allowed: "+input, nil) + } + return safeName, nil } // ResolvePathWithinRoot resolves a validated path element beneath a root directory. diff --git a/agentci/security_test.go b/agentci/security_test.go index cd808b2..069c86e 100644 --- a/agentci/security_test.go +++ b/agentci/security_test.go @@ -21,6 +21,9 @@ func TestSanitizePath_Good(t *testing.T) { {"with.dot", "with.dot"}, {"CamelCase", "CamelCase"}, {"123", "123"}, + {"../secret", "secret"}, + {"/var/tmp/report.txt", "report.txt"}, + {"nested/path/file", "file"}, } for _, tt := range tests { @@ -44,8 +47,8 @@ func TestSanitizePath_Bad(t *testing.T) { {"pipe", "file|name"}, {"ampersand", "file&name"}, {"dollar", "file$name"}, - {"slash", "path/to/file.txt"}, {"backslash", `path\to\file.txt`}, + {"current dir", "."}, {"parent traversal base", ".."}, {"root", "/"}, {"empty", ""},