| package web |
|
|
| import ( |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "testing" |
| ) |
|
|
| |
| |
| func TestSafePath(t *testing.T) { |
| tests := []struct { |
| name string |
| base string |
| path string |
| wantErr bool |
| }{ |
| {"valid relative", "/tmp/state", "pdfs/out.pdf", false}, |
| {"absolute inside rejected", "/tmp/state", "/tmp/state/pdfs/out.pdf", true}, |
| {"traversal dotdot", "/tmp/state", "../etc/passwd", true}, |
| {"traversal absolute", "/tmp/state", "/etc/passwd", true}, |
| {"traversal hidden", "/tmp/state", "pdfs/../../etc/passwd", true}, |
| {"absolute base rejected", "/tmp/state", "/tmp/state", true}, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| _, err := SafePath(tt.base, tt.path) |
| if (err != nil) != tt.wantErr { |
| t.Errorf("SafePath(%q, %q) error = %v, wantErr %v", tt.base, tt.path, err, tt.wantErr) |
| } |
| }) |
| } |
| } |
|
|
| |
| |
| func TestSafePath_TempDir(t *testing.T) { |
| |
| base := t.TempDir() |
| child := filepath.Join(base, "sub", "dir") |
| if err := os.MkdirAll(child, 0o755); err != nil { |
| t.Fatalf("setup: %v", err) |
| } |
|
|
| t.Run("relative inside base", func(t *testing.T) { |
| got, err := SafePath(base, filepath.Join("sub", "dir")) |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
| if got != child { |
| t.Errorf("got %q, want %q", got, child) |
| } |
| }) |
|
|
| t.Run("absolute inside base rejected", func(t *testing.T) { |
| _, err := SafePath(base, child) |
| if err == nil { |
| t.Error("expected error for absolute path, got nil") |
| } |
| }) |
|
|
| t.Run("absolute base itself rejected", func(t *testing.T) { |
| _, err := SafePath(base, base) |
| if err == nil { |
| t.Error("expected error for absolute path, got nil") |
| } |
| }) |
|
|
| t.Run("dotdot escapes base", func(t *testing.T) { |
| _, err := SafePath(base, filepath.Join("..", "escape")) |
| if err == nil { |
| t.Error("expected error for dot-dot traversal, got nil") |
| } |
| }) |
|
|
| t.Run("deep dotdot escapes base", func(t *testing.T) { |
| _, err := SafePath(base, filepath.Join("sub", "..", "..", "escape")) |
| if err == nil { |
| t.Error("expected error for deep dot-dot traversal, got nil") |
| } |
| }) |
|
|
| |
| sibling := filepath.Join(filepath.Dir(base), "sibling") |
| if err := os.MkdirAll(sibling, 0o755); err != nil { |
| t.Fatalf("setup sibling: %v", err) |
| } |
| defer func() { _ = os.RemoveAll(sibling) }() |
|
|
| t.Run("absolute sibling rejected", func(t *testing.T) { |
| _, err := SafePath(base, sibling) |
| if err == nil { |
| t.Errorf("expected error for sibling path %q, got nil", sibling) |
| } |
| }) |
| } |
|
|
| |
| |
| |
| func TestSafePath_RootRelative(t *testing.T) { |
| base := t.TempDir() |
|
|
| attacks := []string{ |
| "/etc/passwd", |
| "/Windows/System32/config/SAM", |
| } |
| for _, p := range attacks { |
| t.Run(p, func(t *testing.T) { |
| _, err := SafePath(base, p) |
| if err == nil { |
| t.Errorf("SafePath(%q, %q) should reject root-relative path", base, p) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestSafePath_Windows(t *testing.T) { |
| if runtime.GOOS != "windows" { |
| t.Skip("Windows-specific test cases") |
| } |
|
|
| base := t.TempDir() |
|
|
| tests := []struct { |
| name string |
| path string |
| }{ |
| {"backslash root", `\Windows\System32\config\SAM`}, |
| {"other drive letter", `D:\secret\file.txt`}, |
| {"UNC path", `\\server\share\secret.txt`}, |
| {"slash root", `/Windows/System32`}, |
| {"mixed separators escape", `..\..\..\Windows\System32`}, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| _, err := SafePath(base, tt.path) |
| if err == nil { |
| t.Errorf("SafePath(%q, %q) should reject path outside base", base, tt.path) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestSafePath_EmptyAndDot(t *testing.T) { |
| base := t.TempDir() |
|
|
| for _, p := range []string{".", ""} { |
| t.Run("path="+p, func(t *testing.T) { |
| got, err := SafePath(base, p) |
| if err != nil { |
| t.Fatalf("unexpected error for %q: %v", p, err) |
| } |
| if got != base { |
| t.Errorf("got %q, want %q", got, base) |
| } |
| }) |
| } |
| } |
|
|
| |
| |
| |
| func TestSafePath_PrefixBoundary(t *testing.T) { |
| parent := t.TempDir() |
|
|
| base := filepath.Join(parent, "state") |
| evil := filepath.Join(parent, "stateEVIL", "secret") |
|
|
| if err := os.MkdirAll(base, 0o755); err != nil { |
| t.Fatalf("setup base: %v", err) |
| } |
| if err := os.MkdirAll(evil, 0o755); err != nil { |
| t.Fatalf("setup evil: %v", err) |
| } |
|
|
| _, err := SafePath(base, evil) |
| if err == nil { |
| t.Errorf("SafePath(%q, %q) should reject prefix-boundary attack", base, evil) |
| } |
| } |
|
|
| |
| |
| func TestSafePath_NullByte(t *testing.T) { |
| base := t.TempDir() |
|
|
| attacks := []string{ |
| "safe\x00/../../../etc/passwd", |
| "\x00/etc/passwd", |
| "sub/\x00", |
| } |
| for _, p := range attacks { |
| t.Run("", func(t *testing.T) { |
| result, err := SafePath(base, p) |
| |
| if err == nil && !strings.HasPrefix(result, base) { |
| t.Errorf("SafePath(%q, %q) = %q — escaped base!", base, p, result) |
| } |
| }) |
| } |
| } |
|
|
| |
| |
| func TestSafePath_RelativeNormalizesInside(t *testing.T) { |
| base := t.TempDir() |
| sub := filepath.Join(base, "a", "b") |
| if err := os.MkdirAll(sub, 0o755); err != nil { |
| t.Fatalf("setup: %v", err) |
| } |
|
|
| |
| got, err := SafePath(base, filepath.Join("a", "b", "..", "b")) |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
| if got != sub { |
| t.Errorf("got %q, want %q", got, sub) |
| } |
| } |
|
|
| |
| func TestSafePath_ReturnValueIsAbsolute(t *testing.T) { |
| base := t.TempDir() |
|
|
| for _, p := range []string{".", "", "sub/file.txt"} { |
| t.Run("path="+p, func(t *testing.T) { |
| got, err := SafePath(base, p) |
| if err != nil { |
| t.Fatalf("unexpected error for %q: %v", p, err) |
| } |
| if !filepath.IsAbs(got) { |
| t.Errorf("SafePath(%q, %q) returned non-absolute path %q", base, p, got) |
| } |
| }) |
| } |
|
|
| |
| t.Run("absolute user path rejected", func(t *testing.T) { |
| _, err := SafePath(base, base) |
| if err == nil { |
| t.Error("expected error for absolute user path") |
| } |
| }) |
| } |
|
|