v0.328.0 (unreleased): the security review's four findings — the entry is bound to its image and start; symlinks out of a root; the missed classes; roots only under the drive namespace or system data
gates / gates (push) Successful in 42s
gates / gates (push) Successful in 42s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -267,3 +267,84 @@ func TestAllowList_CatalogCopyIsIdentical(t *testing.T) {
|
||||
t.Fatalf("%s differs from internal/privpolicy/allowlist.json — change both in the same session", p)
|
||||
}
|
||||
}
|
||||
|
||||
// Security review 2026-10-17, finding 1: an allow-list entry holds only for its image and the image's own start. The
|
||||
// same allowed socket under a swapped image, or with an entrypoint or command added, is refused.
|
||||
func TestAdmit_TheAllowedSettingIsBoundToItsImageAndStart(t *testing.T) {
|
||||
for name, compose := range map[string]string{
|
||||
"another image": strings.Replace(homepage, "ghcr.io/gethomepage/homepage:v1.13.2", "docker.io/evil/homepage:v1.13.2", 1),
|
||||
"added entrypoint": strings.Replace(homepage, " volumes:", " entrypoint: [\"sh\", \"-c\", \"id\"]\n volumes:", 1),
|
||||
"added command": strings.Replace(homepage, " volumes:", " command: sh -c id\n volumes:", 1),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := admit(t, "homepage", compose, env()); err == nil {
|
||||
t.Fatal("admitted")
|
||||
}
|
||||
})
|
||||
}
|
||||
// Positive control: a new TAG or a digest of the same repository is still homepage.
|
||||
bumped := strings.Replace(homepage, "homepage:v1.13.2", "homepage:v1.14.0@sha256:"+strings.Repeat("a", 64), 1)
|
||||
if err := admit(t, "homepage", bumped, env()); err != nil {
|
||||
t.Fatalf("a version bump of the allowed image was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finding 2: on the box, a bind source inside a root that is a symlink OUT of it is outside.
|
||||
func TestAdmit_ASymlinkOutOfARootIsOutside(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
ns := filepath.Join(root, "mnt", "felhom-drives", "d", "felhom-data")
|
||||
if err := os.MkdirAll(filepath.Join(ns, "userdata", "media"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("/etc", filepath.Join(ns, "userdata", "escape")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The roots must be under the drive namespace by NAME; the temp dir stands in via a Resolve that maps the names.
|
||||
e := map[string]string{"HDD_PATH": "/mnt/felhom-drives/d/felhom-data", "USERDATA_PATH": "/mnt/felhom-drives/d/felhom-data/userdata"}
|
||||
p := Policy{Resolve: func(s string) (string, error) {
|
||||
r, err := filepath.EvalSymlinks(filepath.Join(root, s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rel, rerr := filepath.Rel(root, r); rerr == nil && !strings.HasPrefix(rel, "..") {
|
||||
return "/" + rel, nil
|
||||
}
|
||||
return r, nil
|
||||
}}
|
||||
esc := "services:\n app:\n image: x/y:1\n volumes:\n - ${USERDATA_PATH}/escape:/x\n"
|
||||
if err := Admit("app", []byte(esc), e, p); err == nil {
|
||||
t.Fatal("a symlink to /etc inside the userdata root was admitted")
|
||||
}
|
||||
ok := "services:\n app:\n image: x/y:1\n volumes:\n - ${USERDATA_PATH}/media:/m\n - ${USERDATA_PATH}/not-yet:/n\n"
|
||||
if err := Admit("app", []byte(ok), e, p); err != nil {
|
||||
t.Fatalf("a real directory (and one not created yet) inside the root was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finding 3: the classes the first version missed.
|
||||
func TestAdmit_TheLaterClasses(t *testing.T) {
|
||||
for name, add := range map[string]string{
|
||||
"cgroup_parent": " cgroup_parent: /\n",
|
||||
"runtime": " runtime: runc-unsafe\n",
|
||||
"gpus": " gpus: all\n",
|
||||
"device request": " deploy:\n resources:\n reservations:\n devices:\n - capabilities: [gpu]\n",
|
||||
"provider": " provider:\n type: model\n",
|
||||
"post_start": " post_start:\n - command: id\n privileged: true\n",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
compose := strings.Replace(base, " image: example/app:1.0\n", " image: example/app:1.0\n"+add, 1)
|
||||
if err := admit(t, "app", compose, env()); err == nil {
|
||||
t.Fatal("admitted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Finding 4: a data root lives under the drive namespace or the system data path, not anywhere under /mnt.
|
||||
func TestAdmit_ARootElsewhereUnderMntIsNoRoot(t *testing.T) {
|
||||
e := env()
|
||||
e["HDD_PATH"] = "/mnt/sys_other/x"
|
||||
if err := admit(t, "app", base, e); err == nil {
|
||||
t.Fatal("a root outside /mnt/felhom-drives/ and the system data path was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user