Refuse launch dirs overlapping sensitive dirs under every root
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

This commit is contained in:
2026-10-24 17:56:54 +11:00
parent 80c90769fd
commit 9958b5557e
2 changed files with 47 additions and 8 deletions
+16 -5
View File
@@ -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
+31 -3
View File
@@ -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) {