1package confine23import (4 "errors"5 "os"6 "path/filepath"7 "testing"8)910// served builds a served directory next to a private one:11//12// served/13// a.012, sub/b.csv, .env, .hidden/c.01214// in -> a.012 (link inside)15// subln -> sub (link to a directory inside)16// out -> ../private/secret.01217// outdir -> ../private18// abs -> /etc/passwd style absolute link to private19// dangling -> ../private/nothing20// toenv -> .env (link to a hidden file inside)21// private/secret.01222func served(t *testing.T) (Root, string) {23 t.Helper()24 top := t.TempDir()25 dir, priv := filepath.Join(top, "served"), filepath.Join(top, "private")26 for _, d := range []string{dir, priv, filepath.Join(dir, "sub"), filepath.Join(dir, ".hidden")} {27 if err := os.MkdirAll(d, 0o755); err != nil {28 t.Fatal(err)29 }30 }31 for _, f := range []string{"served/a.012", "served/sub/b.csv", "served/.env", "served/.hidden/c.012", "private/secret.012"} {32 if err := os.WriteFile(filepath.Join(top, f), []byte("x"), 0o644); err != nil {33 t.Fatal(err)34 }35 }36 links := map[string]string{37 "in": "a.012",38 "subln": "sub",39 "out": "../private/secret.012",40 "outdir": "../private",41 "abs": filepath.Join(priv, "secret.012"),42 "dangling": "../private/nothing",43 "toenv": ".env",44 "loop": "loop",45 }46 for name, target := range links {47 if err := os.Symlink(target, filepath.Join(dir, name)); err != nil {48 t.Fatal(err)49 }50 }51 r, err := New(dir)52 if err != nil {53 t.Fatal(err)54 }55 return r, r.Dir()56}5758func TestResolve(t *testing.T) {59 r, dir := served(t)60 ok := []struct{ name, want string }{61 {"a.012", "a.012"},62 {"./a.012", "a.012"},63 {"sub/b.csv", "sub/b.csv"},64 {"sub/../a.012", "a.012"},65 {"new.012", "new.012"},66 {"sub/new.csv", "sub/new.csv"},67 {"missing/dir/new.012", "missing/dir/new.012"},68 {"in", "a.012"},69 {"subln/b.csv", "sub/b.csv"},70 {".", "."},71 {"a b.012", "a b.012"},72 }73 for _, c := range ok {74 got, err := r.Resolve(c.name)75 want := filepath.Join(dir, c.want)76 if err != nil || got != want {77 t.Errorf("Resolve(%q) = %q, %v; want %q", c.name, got, err, want)78 }79 }80 escapes := []string{81 "/etc/passwd",82 filepath.Join(dir, "a.012"), // absolute, even inside83 "..",84 "../private/secret.012",85 "sub/../../private/secret.012",86 "sub/../..",87 "./../served/a.012",88 ".env",89 ".hidden/c.012",90 "sub/.x.csv",91 "new/.env",92 "out",93 "outdir/secret.012",94 "abs",95 "dangling",96 "toenv",97 "loop",98 `\\server\share`,99 }100 for _, name := range escapes {101 got, err := r.Resolve(name)102 if !errors.Is(err, ErrOutside) {103 t.Errorf("Resolve(%q) = %q, %v; want ErrOutside", name, got, err)104 }105 }106 if _, err := r.Resolve(""); err == nil {107 t.Error("Resolve(\"\") succeeded")108 }109}110111func TestUnconfined(t *testing.T) {112 var r Root113 for _, name := range []string{"/etc/passwd", "../x", ".env", "a.012"} {114 if got, err := r.Resolve(name); err != nil || got != name {115 t.Errorf("Resolve(%q) = %q, %v; want it unchanged", name, got, err)116 }117 }118 if r.Confined() {119 t.Error("the zero Root is confined")120 }121}122123func TestNew(t *testing.T) {124 if _, err := New(filepath.Join(t.TempDir(), "missing")); err == nil {125 t.Error("New of a missing directory succeeded")126 }127 f := filepath.Join(t.TempDir(), "file")128 os.WriteFile(f, nil, 0o644)129 if _, err := New(f); err == nil {130 t.Error("New of a file succeeded")131 }132 // A root reached through a link is resolved, so paths under it compare.133 top := t.TempDir()134 os.Mkdir(filepath.Join(top, "real"), 0o755)135 os.Symlink("real", filepath.Join(top, "link"))136 r, err := New(filepath.Join(top, "link"))137 if err != nil {138 t.Fatal(err)139 }140 real, _ := filepath.EvalSymlinks(filepath.Join(top, "real"))141 if r.Dir() != real {142 t.Errorf("Dir = %q, want %q", r.Dir(), real)143 }144}