zsvo/pkg/security/path_validator_test.go
2026-03-13 15:18:31 +03:00

289 lines
5.8 KiB
Go

package security
import (
"path/filepath"
"runtime"
"testing"
)
func TestPathValidator_ValidatePath(t *testing.T) {
t.Parallel()
cases := []struct {
name string
path string
strict bool
wantError bool
}{
{
name: "valid relative path",
path: "usr/bin/test",
strict: true,
wantError: false,
},
{
name: "path traversal attempt",
path: "../../../etc/passwd",
strict: true,
wantError: true,
},
{
name: "absolute path in strict mode",
path: "/usr/bin/test",
strict: true,
wantError: true,
},
{
name: "absolute path in non-strict mode",
path: "/usr/bin/test",
strict: false,
wantError: false,
},
{
name: "suspicious characters",
path: "test$HOME",
strict: true,
wantError: true,
},
{
name: "hidden file with dots only",
path: "...",
strict: true,
wantError: true,
},
{
name: "valid hidden file",
path: ".config",
strict: true,
wantError: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
validator := NewPathValidator(nil, tc.strict)
err := validator.ValidatePath(tc.path)
gotError := err != nil
if gotError != tc.wantError {
t.Errorf("ValidatePath(%q) error = %v, wantError %v", tc.path, err, tc.wantError)
}
})
}
}
func TestPathValidator_SanitizePath(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
expected string
}{
{
name: "normal path",
input: "usr/bin/test",
expected: "usr/bin/test",
},
{
name: "path with null bytes",
input: "test\x00file",
expected: "testfile",
},
{
name: "path with forward slashes",
input: "usr/bin/test",
expected: joinOSPath("usr", "bin", "test"),
},
{
name: "path with consecutive separators",
input: "usr//bin///test",
expected: joinOSPath("usr", "bin", "test"),
},
{
name: "path with . and ..",
input: "usr/./bin/../test",
expected: joinOSPath("usr", "test"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
validator := NewPathValidator(nil, true)
result := validator.SanitizePath(tc.input)
if result != tc.expected {
t.Errorf("SanitizePath(%q) = %q, expected %q", tc.input, result, tc.expected)
}
})
}
}
func TestPathValidator_ValidateFileName(t *testing.T) {
t.Parallel()
cases := []struct {
name string
filename string
wantError bool
}{
{
name: "valid filename",
filename: "test.txt",
wantError: false,
},
{
name: "empty filename",
filename: "",
wantError: true,
},
{
name: "filename too long",
filename: string(make([]byte, 256)),
wantError: true,
},
{
name: "filename with trailing whitespace",
filename: "test ",
wantError: true,
},
}
// Add platform-specific tests
if runtime.GOOS == "windows" {
cases = append(cases, []struct {
name string
filename string
wantError bool
}{
{
name: "windows reserved name",
filename: "CON",
wantError: true,
},
{
name: "windows invalid characters",
filename: "test<file",
wantError: true,
},
}...)
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
validator := NewPathValidator(nil, true)
err := validator.ValidateFileName(tc.filename)
gotError := err != nil
if gotError != tc.wantError {
t.Errorf("ValidateFileName(%q) error = %v, wantError %v", tc.filename, err, tc.wantError)
}
})
}
}
func TestPathValidator_SafeJoin(t *testing.T) {
t.Parallel()
validator := NewPathValidator(nil, false) // Non-strict mode
cases := []struct {
name string
base string
path string
wantError bool
expected string
}{
{
name: "valid join",
base: "/usr",
path: "bin/test",
wantError: false,
expected: joinOSPath("/usr", "bin", "test"),
},
{
name: "path traversal attempt",
base: "/usr",
path: "../../../etc/passwd",
wantError: true,
},
{
name: "empty path",
base: "/usr",
path: "",
wantError: false,
expected: "/usr",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := validator.SafeJoin(tc.base, tc.path)
if tc.wantError {
if err == nil {
t.Errorf("SafeJoin(%q, %q) expected error", tc.base, tc.path)
}
} else {
if err != nil {
t.Errorf("SafeJoin(%q, %q) unexpected error: %v", tc.base, tc.path, err)
}
if result != tc.expected {
t.Errorf("SafeJoin(%q, %q) = %q, expected %q", tc.base, tc.path, result, tc.expected)
}
}
})
}
}
func TestPathValidator_AllowedPaths(t *testing.T) {
t.Parallel()
allowed := []string{"/tmp", "/var/tmp"}
validator := NewPathValidator(allowed, false) // Non-strict mode for absolute paths
cases := []struct {
name string
path string
wantError bool
}{
{
name: "allowed path",
path: "/tmp/test",
wantError: false,
},
{
name: "disallowed path",
path: "/etc/passwd",
wantError: true,
},
{
name: "subdirectory of allowed path",
path: "/var/tmp/subdir/file",
wantError: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := validator.ValidatePath(tc.path)
gotError := err != nil
if gotError != tc.wantError {
t.Errorf("ValidatePath(%q) error = %v, wantError %v", tc.path, err, tc.wantError)
}
})
}
}
// Helper function for cross-platform path joining
func joinOSPath(parts ...string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, part := range parts[1:] {
result += string(filepath.Separator) + part
}
return result
}