Parent directory

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}