package rootstate import ( "io" "os" "path/filepath" "testing" "golang.org/x/sys/unix" ) func TestPlan14AuthorityFilePrivateAncestors(t *testing.T) { for _, attack := range []string{"none", "public-leaf-parent", "writable-anchor", "writable-ancestor", "writable-leaf", "ancestor-link", "leaf-link", "hardlink", "fifo", "directory", "wrong-owner", "missing", "relative-component"} { t.Run(attack, func(t *testing.T) { root := t.TempDir() if err := os.Chmod(root, 0700); err != nil { t.Fatal(err) } path := filepath.Join(root, "private", "nested", "journal") if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("retained"), 0600); err != nil { t.Fatal(err) } must := func(err error) { t.Helper() if err != nil { t.Fatal(err) } } owner := uint32(os.Geteuid()) parts := []string{"private", "nested", "journal"} switch attack { case "public-leaf-parent": must(os.Chmod(filepath.Dir(path), 0755)) case "writable-anchor": must(os.Chmod(root, 0777)) case "writable-ancestor": must(os.Chmod(filepath.Join(root, "private"), 0777)) case "writable-leaf": must(os.Chmod(path, 0666)) case "ancestor-link": must(os.Rename(filepath.Join(root, "private"), filepath.Join(root, "saved"))) must(os.Symlink("saved", filepath.Join(root, "private"))) case "leaf-link": must(os.Rename(path, path+".saved")) must(os.Symlink("journal.saved", path)) case "hardlink": must(os.Link(path, path+".alias")) case "fifo": must(os.Remove(path)) must(unix.Mkfifo(path, 0600)) case "directory": must(os.Remove(path)) must(os.Mkdir(path, 0700)) case "wrong-owner": owner++ case "missing": must(os.Remove(path)) case "relative-component": parts = []string{"private", "nested", "..", "nested", "journal"} } parent, err := os.Open(root) must(err) defer parent.Close() f, err := openAuthorityFile(parent, parts, owner) if attack != "none" && attack != "public-leaf-parent" { if err == nil { f.Close() t.Fatal("hostile authority file accepted") } return } if err != nil { t.Fatalf("private authority file refused: %v", err) } defer f.Close() raw, err := io.ReadAll(f) if err != nil || string(raw) != "retained" { t.Fatalf("retained bytes: %q %v", raw, err) } }) } } func TestPlan14AuthorityFileAbsoluteBoundary(t *testing.T) { for _, path := range []string{"relative", "/etc/../etc/passwd", "/", filepath.Join(t.TempDir(), "missing")} { if f, err := OpenAuthorityFile(path); err == nil { f.Close() t.Fatalf("untrusted path accepted: %q", path) } } t.Run("public-root-owned-file", func(t *testing.T) { for _, path := range []string{"/", "/usr", "/usr/bin", "/usr/bin/true"} { var st unix.Stat_t if err := unix.Lstat(path, &st); err != nil { t.Fatal(err) } if st.Uid != 0 { t.Skipf("root-owned fixture unavailable: %s uid=%d", path, st.Uid) } } f, err := OpenAuthorityFile("/usr/bin/true") if err != nil { t.Fatal(err) } f.Close() }) }