Diff
1diff --git a/pkg/storage/local_test.go b/pkg/storage/local_test.go
2index aab7b423dbab24ff0a79dd099e766df52414be4d..56532185b1577a5bc6ed29bdcf28e830593a2f2a 100644
3--- a/pkg/storage/local_test.go
4+++ b/pkg/storage/local_test.go
5@@ -18,14 +18,22 @@ func TestLocalStorageConfinesToRoot(t *testing.T) {
6 t.Fatal(err)
7 }
8
9- l := NewLocalStorage(root)
10- for _, name := range []string{
11+ names := []string{
12 "../secret",
13 "objects/../../secret",
14 "../../../../../../../../etc/passwd",
15 secret,
16- "/etc/passwd",
17- } {
18+ }
19+
20+ // A leading slash only means "absolute" where there is no volume name. On
21+ // Windows "/etc/passwd" is a relative path, and Join confines it under the
22+ // root rather than escaping, so there is nothing to reject.
23+ if filepath.IsAbs("/etc/passwd") {
24+ names = append(names, "/etc/passwd")
25+ }
26+
27+ l := NewLocalStorage(root)
28+ for _, name := range names {
29 t.Run(name, func(t *testing.T) {
30 if _, err := l.Open(name); !errors.Is(err, ErrPathTraversal) {
31 t.Errorf("Open: got %v, want ErrPathTraversal", err)
32@@ -59,6 +67,24 @@ func TestLocalStorageConfinesToRoot(t *testing.T) {
33 }
34 }
35
36+// Where a leading slash carries no volume name, "/etc/passwd" is a relative
37+// name rather than an absolute one. It must still land under the root, which is
38+// the property that matters on those platforms.
39+func TestLocalStorageConfinesRootedRelativeNames(t *testing.T) {
40+ if filepath.IsAbs("/etc/passwd") {
41+ t.Skip("a leading slash is absolute here, covered by the rejection test")
42+ }
43+
44+ root := t.TempDir()
45+ l := NewLocalStorage(root)
46+ if _, err := l.Put("/etc/passwd", strings.NewReader("x")); err != nil {
47+ t.Fatalf("Put: %v", err)
48+ }
49+ if _, err := os.Stat(filepath.Join(root, "etc", "passwd")); err != nil {
50+ t.Errorf("write should have been confined under root: %v", err)
51+ }
52+}
53+
54 // Ordinary relative names still round-trip through the root. This is the shape
55 // of an LFS upload: stage under "incomplete", then rename into place. Names
56 // crossing the Storage boundary are relative, so callers must not feed back the