package safefile import ( "io" "os" "path/filepath" "testing" "golang.org/x/sys/unix" ) func TestRefuseUnsafeOpen(t *testing.T) { for _, kind := range []string{"symlink", "hardlink", "fifo", "parent"} { t.Run(kind, func(t *testing.T) { dir := t.TempDir() victim := filepath.Join(dir, "victim") if err := os.WriteFile(victim, []byte("untouched"), 0o600); err != nil { t.Fatal(err) } path := filepath.Join(dir, "unsafe") var err error switch kind { case "symlink": err = os.Symlink(victim, path) case "hardlink": err = os.Link(victim, path) case "fifo": err = unix.Mkfifo(path, 0o600) case "parent": err = os.Symlink(dir, path) path = filepath.Join(path, "victim") } if err != nil { t.Fatal(err) } f, err := OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) if err == nil { f.Close() t.Fatal("unsafe path opened") } got, err := os.ReadFile(victim) if err != nil || string(got) != "untouched" { t.Fatalf("victim changed: %q %v", got, err) } }) } } func TestWritePinsParentAndTemporaryFile(t *testing.T) { dir := t.TempDir() parent := filepath.Join(dir, "state") outside := filepath.Join(dir, "outside") for _, p := range []string{parent, outside} { if err := os.Mkdir(p, 0o755); err != nil { t.Fatal(err) } } victim := filepath.Join(outside, "data") if err := os.WriteFile(victim, []byte("untouched"), 0o600); err != nil { t.Fatal(err) } moved := filepath.Join(dir, "moved") err := Write(filepath.Join(parent, "result"), 0o644, func(w io.Writer) error { entries, err := os.ReadDir(parent) if err != nil { return err } stage := filepath.Join(parent, entries[0].Name()) if err := os.Rename(stage, stage+"-moved"); err != nil { return err } if err := os.Symlink(outside, stage); err != nil { return err } if err := os.Rename(parent, moved); err != nil { return err } if err := os.Symlink(outside, parent); err != nil { return err } _, err = io.WriteString(w, "safe") return err }) if err != nil { t.Fatal(err) } got, err := os.ReadFile(filepath.Join(moved, "result")) if err != nil || string(got) != "safe" { t.Fatalf("anchored result: %q %v", got, err) } got, err = os.ReadFile(victim) if err != nil || string(got) != "untouched" { t.Fatalf("victim changed: %q %v", got, err) } st, err := os.Stat(victim) if err != nil || st.Mode().Perm() != 0o600 { t.Fatalf("victim permissions changed: %v %v", st, err) } if _, err := os.Stat(filepath.Join(outside, "result")); !os.IsNotExist(err) { t.Fatalf("published outside pinned parent: %v", err) } }