carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package rootstate
2
3import (
4 "io"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "golang.org/x/sys/unix"
10)
11
12func TestPlan14AuthorityFilePrivateAncestors(t *testing.T) {
13 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"} {
14 t.Run(attack, func(t *testing.T) {
15 root := t.TempDir()
16 if err := os.Chmod(root, 0700); err != nil {
17 t.Fatal(err)
18 }
19 path := filepath.Join(root, "private", "nested", "journal")
20 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
21 t.Fatal(err)
22 }
23 if err := os.WriteFile(path, []byte("retained"), 0600); err != nil {
24 t.Fatal(err)
25 }
26 must := func(err error) {
27 t.Helper()
28 if err != nil {
29 t.Fatal(err)
30 }
31 }
32 owner := uint32(os.Geteuid())
33 parts := []string{"private", "nested", "journal"}
34 switch attack {
35 case "public-leaf-parent":
36 must(os.Chmod(filepath.Dir(path), 0755))
37 case "writable-anchor":
38 must(os.Chmod(root, 0777))
39 case "writable-ancestor":
40 must(os.Chmod(filepath.Join(root, "private"), 0777))
41 case "writable-leaf":
42 must(os.Chmod(path, 0666))
43 case "ancestor-link":
44 must(os.Rename(filepath.Join(root, "private"), filepath.Join(root, "saved")))
45 must(os.Symlink("saved", filepath.Join(root, "private")))
46 case "leaf-link":
47 must(os.Rename(path, path+".saved"))
48 must(os.Symlink("journal.saved", path))
49 case "hardlink":
50 must(os.Link(path, path+".alias"))
51 case "fifo":
52 must(os.Remove(path))
53 must(unix.Mkfifo(path, 0600))
54 case "directory":
55 must(os.Remove(path))
56 must(os.Mkdir(path, 0700))
57 case "wrong-owner":
58 owner++
59 case "missing":
60 must(os.Remove(path))
61 case "relative-component":
62 parts = []string{"private", "nested", "..", "nested", "journal"}
63 }
64 parent, err := os.Open(root)
65 must(err)
66 defer parent.Close()
67 f, err := openAuthorityFile(parent, parts, owner)
68 if attack != "none" && attack != "public-leaf-parent" {
69 if err == nil {
70 f.Close()
71 t.Fatal("hostile authority file accepted")
72 }
73 return
74 }
75 if err != nil {
76 t.Fatalf("private authority file refused: %v", err)
77 }
78 defer f.Close()
79 raw, err := io.ReadAll(f)
80 if err != nil || string(raw) != "retained" {
81 t.Fatalf("retained bytes: %q %v", raw, err)
82 }
83 })
84 }
85}
86
87func TestPlan14AuthorityFileAbsoluteBoundary(t *testing.T) {
88 for _, path := range []string{"relative", "/etc/../etc/passwd", "/", filepath.Join(t.TempDir(), "missing")} {
89 if f, err := OpenAuthorityFile(path); err == nil {
90 f.Close()
91 t.Fatalf("untrusted path accepted: %q", path)
92 }
93 }
94 t.Run("public-root-owned-file", func(t *testing.T) {
95 for _, path := range []string{"/", "/usr", "/usr/bin", "/usr/bin/true"} {
96 var st unix.Stat_t
97 if err := unix.Lstat(path, &st); err != nil {
98 t.Fatal(err)
99 }
100 if st.Uid != 0 {
101 t.Skipf("root-owned fixture unavailable: %s uid=%d", path, st.Uid)
102 }
103 }
104 f, err := OpenAuthorityFile("/usr/bin/true")
105 if err != nil {
106 t.Fatal(err)
107 }
108 f.Close()
109 })
110}
111