fetch_ml/tests/unit/security/path_traversal_test.go
Jeremie Fraeys fccced6bb3
test(security): add comprehensive security unit tests
Adds 13 security tests across 4 files for hardening verification:

**Path Traversal Tests (path_traversal_test.go):**
- TestSecurePathValidator_ValidRelativePath
- TestSecurePathValidator_PathTraversalBlocked
- TestSecurePathValidator_SymlinkEscape
- Tests symlink resolution and path boundary enforcement

**File Type Validation Tests (filetype_test.go):**
- TestValidateFileType_AllowedTypes
- TestValidateFileType_DangerousTypesBlocked
- TestValidateModelFile
- Tests magic bytes validation and dangerous extension blocking

**Secrets Management Tests (secrets_test.go):**
- TestExpandSecrets_BasicExpansion
- TestExpandSecrets_NestedAndMissingVars
- TestValidateNoPlaintextSecrets_HeuristicDetection
- Tests env variable expansion and plaintext secret detection with entropy

**Audit Logging Tests (audit_test.go):**
- TestAuditLogger_ChainIntegrity
- TestAuditLogger_VerifyChain
- TestAuditLogger_LogFileAccess
- TestAuditLogger_Disabled
- Tests tamper-evident chain hashing and file access logging
2026-02-23 18:00:45 -05:00

120 lines
2.9 KiB
Go

package security
import (
"os"
"path/filepath"
"testing"
"github.com/jfraeys/fetch_ml/internal/fileutil"
)
func TestSecurePathValidator_ValidatePath(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
validator := fileutil.NewSecurePathValidator(tempDir)
tests := []struct {
name string
input string
wantErr bool
errMsg string
}{
{
name: "valid relative path",
input: "subdir/file.txt",
wantErr: false,
},
{
name: "valid absolute path within base",
input: filepath.Join(tempDir, "file.txt"),
wantErr: false,
},
{
name: "path traversal attempt with dots",
input: "../etc/passwd",
wantErr: true,
errMsg: "path escapes base directory",
},
{
name: "path traversal attempt with encoded dots",
input: "...//...//etc/passwd",
wantErr: true,
errMsg: "path escapes base directory",
},
{
name: "absolute path outside base",
input: "/etc/passwd",
wantErr: true,
errMsg: "path escapes base directory",
},
{
name: "empty path returns base",
input: "",
wantErr: false,
},
{
name: "single dot current directory",
input: ".",
wantErr: false,
},
}
// Create subdir for tests that need it
_ = os.MkdirAll(filepath.Join(tempDir, "subdir"), 0755)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := validator.ValidatePath(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("ValidatePath() error = nil, wantErr %v", tt.wantErr)
return
}
if tt.errMsg != "" && err.Error()[:len(tt.errMsg)] != tt.errMsg {
t.Errorf("ValidatePath() error = %v, want %v", err, tt.errMsg)
}
} else {
if err != nil {
t.Errorf("ValidatePath() unexpected error = %v", err)
return
}
if got == "" {
t.Errorf("ValidatePath() returned empty path")
}
}
})
}
}
func TestSecurePathValidator_SymlinkEscape(t *testing.T) {
// Create temp directories
tempDir := t.TempDir()
outsideDir := t.TempDir()
validator := fileutil.NewSecurePathValidator(tempDir)
// Create a file outside the base directory
outsideFile := filepath.Join(outsideDir, "secret.txt")
if err := os.WriteFile(outsideFile, []byte("secret"), 0600); err != nil {
t.Fatalf("Failed to create outside file: %v", err)
}
// Create a symlink inside tempDir pointing outside
symlinkPath := filepath.Join(tempDir, "link")
if err := os.Symlink(outsideFile, symlinkPath); err != nil {
t.Fatalf("Failed to create symlink: %v", err)
}
// Attempt to access through symlink should fail
_, err := validator.ValidatePath("link")
if err == nil {
t.Errorf("Symlink escape should be blocked: %v", err)
}
}
func TestSecurePathValidator_BasePathNotSet(t *testing.T) {
validator := fileutil.NewSecurePathValidator("")
_, err := validator.ValidatePath("test.txt")
if err == nil || err.Error() != "base path not set" {
t.Errorf("Expected 'base path not set' error, got: %v", err)
}
}