TestBootstrapRefusesSymlinkedDir: a co-tenant could plant .codehamr → an attacker-controlled dir before first run. Bootstrap must Lstat (not Stat) and refuse any symlink: even with a 0o600 config.yaml, the attacker owns the parent and can swap or read what codehamr writes. Same defence for a planted
(t *testing.T)
| 604 | // parent and can swap or read what codehamr writes. Same defence for a planted |
| 605 | // config.yaml symlink. |
| 606 | func TestBootstrapRefusesSymlinkedDir(t *testing.T) { |
| 607 | root := t.TempDir() |
| 608 | target := t.TempDir() |
| 609 | link := filepath.Join(root, DirName) |
| 610 | if err := os.Symlink(target, link); err != nil { |
| 611 | t.Skipf("symlink unsupported: %v", err) |
| 612 | } |
| 613 | _, _, err := Bootstrap(root) |
| 614 | if err == nil { |
| 615 | t.Fatal("Bootstrap accepted a symlinked .codehamr - config-injection vector left open") |
| 616 | } |
| 617 | if !strings.Contains(err.Error(), "symlink") { |
| 618 | t.Fatalf("error should name the symlink defence: %v", err) |
| 619 | } |
| 620 | // Target must stay untouched, nothing dropped into the attacker-controlled dir. |
| 621 | if _, err := os.Stat(filepath.Join(target, "config.yaml")); err == nil { |
| 622 | t.Fatal("Bootstrap wrote into the symlink target despite the rejection") |
| 623 | } |
| 624 | } |
| 625 | |
| 626 | func TestBootstrapRefusesSymlinkedConfigYAML(t *testing.T) { |
| 627 | root := t.TempDir() |