local_test.go
3797 bytes
1package storage
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// Names handed to LocalStorage come from LFS object IDs, which are attacker
12// controlled. None of them may resolve outside the root.
13func TestLocalStorageConfinesToRoot(t *testing.T) {
14 outside := t.TempDir()
15 root := filepath.Join(outside, "root")
16 secret := filepath.Join(outside, "secret")
17 if err := os.WriteFile(secret, []byte("host key"), 0o600); err != nil {
18 t.Fatal(err)
19 }
20
21 names := []string{
22 "../secret",
23 "objects/../../secret",
24 "../../../../../../../../etc/passwd",
25 secret,
26 }
27
28 // A leading slash only means "absolute" where there is no volume name. On
29 // Windows "/etc/passwd" is a relative path, and Join confines it under the
30 // root rather than escaping, so there is nothing to reject.
31 if filepath.IsAbs("/etc/passwd") {
32 names = append(names, "/etc/passwd")
33 }
34
35 l := NewLocalStorage(root)
36 for _, name := range names {
37 t.Run(name, func(t *testing.T) {
38 if _, err := l.Open(name); !errors.Is(err, ErrPathTraversal) {
39 t.Errorf("Open: got %v, want ErrPathTraversal", err)
40 }
41 if _, err := l.Stat(name); !errors.Is(err, ErrPathTraversal) {
42 t.Errorf("Stat: got %v, want ErrPathTraversal", err)
43 }
44 if _, err := l.Exists(name); !errors.Is(err, ErrPathTraversal) {
45 t.Errorf("Exists: got %v, want ErrPathTraversal", err)
46 }
47 if _, err := l.Put(name, strings.NewReader("x")); !errors.Is(err, ErrPathTraversal) {
48 t.Errorf("Put: got %v, want ErrPathTraversal", err)
49 }
50 if err := l.Delete(name); !errors.Is(err, ErrPathTraversal) {
51 t.Errorf("Delete: got %v, want ErrPathTraversal", err)
52 }
53 if err := l.Rename("objects/a", name); !errors.Is(err, ErrPathTraversal) {
54 t.Errorf("Rename dst: got %v, want ErrPathTraversal", err)
55 }
56 if err := l.Rename(name, "objects/a"); !errors.Is(err, ErrPathTraversal) {
57 t.Errorf("Rename src: got %v, want ErrPathTraversal", err)
58 }
59 })
60 }
61
62 if _, err := os.Stat(root); !errors.Is(err, os.ErrNotExist) {
63 t.Errorf("root was created by a rejected write: %v", err)
64 }
65 if b, err := os.ReadFile(secret); err != nil || string(b) != "host key" {
66 t.Errorf("secret was clobbered: %q %v", b, err)
67 }
68}
69
70// Where a leading slash carries no volume name, "/etc/passwd" is a relative
71// name rather than an absolute one. It must still land under the root, which is
72// the property that matters on those platforms.
73func TestLocalStorageConfinesRootedRelativeNames(t *testing.T) {
74 if filepath.IsAbs("/etc/passwd") {
75 t.Skip("a leading slash is absolute here, covered by the rejection test")
76 }
77
78 root := t.TempDir()
79 l := NewLocalStorage(root)
80 if _, err := l.Put("/etc/passwd", strings.NewReader("x")); err != nil {
81 t.Fatalf("Put: %v", err)
82 }
83 if _, err := os.Stat(filepath.Join(root, "etc", "passwd")); err != nil {
84 t.Errorf("write should have been confined under root: %v", err)
85 }
86}
87
88// Ordinary relative names still round-trip through the root. This is the shape
89// of an LFS upload: stage under "incomplete", then rename into place. Names
90// crossing the Storage boundary are relative, so callers must not feed back the
91// absolute path an opened Object reports.
92func TestLocalStorageRoundTrip(t *testing.T) {
93 root := t.TempDir()
94 l := NewLocalStorage(root)
95
96 if _, err := l.Put("incomplete/tmp", strings.NewReader("hello")); err != nil {
97 t.Fatal(err)
98 }
99 if err := l.Rename("incomplete/tmp", "objects/ab/cd/abcd"); err != nil {
100 t.Fatal(err)
101 }
102
103 exists, err := l.Exists("objects/ab/cd/abcd")
104 if err != nil || !exists {
105 t.Fatalf("Exists: %v %v", exists, err)
106 }
107
108 f, err := l.Open("objects/ab/cd/abcd")
109 if err != nil {
110 t.Fatal(err)
111 }
112 defer f.Close() //nolint: errcheck
113
114 if got := f.Name(); !strings.HasPrefix(got, root) {
115 t.Errorf("Name %q is not under root %q", got, root)
116 }
117}