diff --git a/main.go b/main.go index 823a691..95ea95d 100644 --- a/main.go +++ b/main.go @@ -339,6 +339,16 @@ func sensitiveDirs(home string) []string { return dirs } +// overlapsSensitive returns the sensitive dir that p is, sits under or contains. +func overlapsSensitive(p, home string) string { + for _, s := range sensitiveDirs(home) { + if s = realpath(s); inside(p, s) || inside(s, p) { + return s + } + } + return "" +} + // allowedRoots reads the launch-dir roots; without the file the only root is // $HOME and def is true. func allowedRoots(home string) (roots []string, def bool, err error) { @@ -378,10 +388,8 @@ func allowedRoots(home string) (roots []string, def bool, err error) { return nil, false, fmt.Errorf("%s: %q is or contains $PATH entry %s", allowedPathsFile(), l, d) } } - for _, s := range sensitiveDirs(home) { - if s = realpath(s); inside(r, s) || inside(s, r) { - return nil, false, fmt.Errorf("%s: %q overlaps %s", allowedPathsFile(), l, s) - } + if s := overlapsSensitive(r, home); s != "" { + return nil, false, fmt.Errorf("%s: %q overlaps %s", allowedPathsFile(), l, s) } roots = append(roots, r) } @@ -399,7 +407,7 @@ func pathDirs() []string { // checkLaunchDir returns pwd resolved if it is strictly under an allowed root, // never $HOME itself, anything holding the sandbox's config, the API socket or -// a $PATH entry, nor anything overlapping a forbidden system dir. Under the +// a $PATH entry, nor anything overlapping a forbidden system or sensitive dir. Under the // default $HOME root, dot-dirs of $HOME are refused too. func checkLaunchDir(pwd, allowlist string) (string, error) { pwd = realpath(pwd) @@ -426,6 +434,9 @@ func checkLaunchDir(pwd, allowlist string) (string, error) { return "", fmt.Errorf("refusing to mount %s: it overlaps %s", pwd, bad) } } + if s := overlapsSensitive(pwd, home); s != "" { + return "", fmt.Errorf("refusing to mount %s: it overlaps %s", pwd, s) + } for _, r := range roots { if pwd != r && inside(pwd, r) { return pwd, nil diff --git a/main_test.go b/main_test.go index 753e279..7f9c20d 100644 --- a/main_test.go +++ b/main_test.go @@ -326,6 +326,18 @@ func TestLaunchRefusals(t *testing.T) { }}, {"invalid-root", "home/proj", func(t *testing.T, e *env) { writePaths(t, e, "/run/x") }}, {"var-tmp-under-var-root", "/var/tmp", func(t *testing.T, e *env) { writePaths(t, e, "/var") }}, + {"xdg-config-subdir", "home/cfg/systemd", func(t *testing.T, e *env) { + _ = os.MkdirAll(e.work+"/home/cfg/systemd", 0o755) + t.Setenv("XDG_CONFIG_HOME", e.work+"/home/cfg") + }}, + {"xdg-data-home", "home/data", func(t *testing.T, e *env) { + _ = os.MkdirAll(e.work+"/home/data", 0o755) + t.Setenv("XDG_DATA_HOME", e.work+"/home/data") + }}, + {"xdg-data-containers", "home/data/containers", func(t *testing.T, e *env) { + _ = os.MkdirAll(e.work+"/home/data/containers", 0o755) + t.Setenv("XDG_DATA_HOME", e.work+"/home/data") + }}, {"default-root-dot-dir", "home/.local/src/x", func(t *testing.T, e *env) { _ = os.MkdirAll(e.work+"/home/.local/src/x", 0o755) }}, @@ -365,6 +377,9 @@ func TestCheckLaunchDirRuntimeFallback(t *testing.T) { forbiddenRoots = []string{"/run"} t.Cleanup(func() { forbiddenRoots = origRoots }) _, err = checkLaunchDir("/tmp/home/proj", "/nonexistent/hosts") + check(t, "under runtime dir", true, err != nil && strings.Contains(err.Error(), "overlaps "+realpath("/tmp"))) + t.Setenv("XDG_RUNTIME_DIR", "/nonexistent/rt") + _, err = checkLaunchDir("/tmp/home/proj", "/nonexistent/hosts") check(t, "subdir", nil, err) } @@ -579,10 +594,11 @@ func TestLaunchDotDirRoot(t *testing.T) { } func TestCheckLaunchDirRoots(t *testing.T) { - cfg := t.TempDir() + cfg, _ := filepath.EvalSymlinks(t.TempDir()) t.Setenv("HOME", "/home/u") t.Setenv("XDG_CONFIG_HOME", cfg) - t.Setenv("XDG_DATA_HOME", "") + t.Setenv("XDG_DATA_HOME", "/srv/data") + t.Setenv("XDG_RUNTIME_DIR", "/srv/rt") t.Setenv("PATH", "/usr/bin:/srv/tools/bin") f := filepath.Join(cfg, "agent-sandbox", "allowed-paths") _ = os.MkdirAll(filepath.Dir(f), 0o755) @@ -596,7 +612,7 @@ func TestCheckLaunchDirRoots(t *testing.T) { check(t, dir, ok, err == nil) } for _, bad := range []string{"/", "/home", "~", "~/", "~/.local", "~/.local/share", "~/.ssh", "~/.ssh/x", - "~/.gnupg", "~/.config/foo", cfg, cfg + "/agent-sandbox/x", "~/.local/share/containers/s", "relative/x", "/run/user/1000", "/tmp/x", "/var/tmp", "/proc/1", + "~/.gnupg", "~/.config/foo", "~/.local/share/containers/s", "relative/x", "/run/user/1000", "/tmp/x", "/var/tmp", "/proc/1", "/sys", "/dev/shm", "/usr", "/srv/tools", "/var"} { write("/srv/work\n" + bad) _, _, err := allowedRoots("/home/u") @@ -605,6 +621,18 @@ func TestCheckLaunchDirRoots(t *testing.T) { write("/var") _, err := checkLaunchDir("/var/tmp/x", "/nowhere/hosts") check(t, "/var/tmp under /var root", true, err != nil) + + // cfg lives under /tmp, so drop that rule to leave only the sensitive one. + origRoots := forbiddenRoots + forbiddenRoots = []string{"/run"} + t.Cleanup(func() { forbiddenRoots = origRoots }) + for bad, want := range map[string]string{cfg: cfg, cfg + "/agent-sandbox/x": cfg, "/srv/data/containers/x": "/srv/data/containers", + "/srv/data": "/srv/data/containers", "~/.ssh/x": "/home/u/.ssh", "~/.gnupg/x": "/home/u/.gnupg", + "~/.config/foo": "/home/u/.config", "~/.local/share/containers/s": "/home/u/.local/share/containers", "/srv/rt/x": "/srv/rt"} { + write("/srv/work\n" + bad) + _, _, err := allowedRoots("/home/u") + check(t, "sensitive root "+bad, true, strings.Contains(fmt.Sprint(err), "overlaps "+want)) + } } func TestLaunchPassesArgsVerbatim(t *testing.T) {