3f382bf762
From AUDIT-blast-radius-hostroot-localapi-2026-07-02.md. Each fix ships with a non-hollow test + a companion red-proof (shown failing on the pre-fix impl). Sudoers install-source grants became globs — deploy the sudoers drop-in with the binary. A1 (stale-lock pool-membership) deliberately excluded (spike). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package guesthook
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"testing"
|
|
)
|
|
|
|
// recordingRunner is a fake proxmox.Runner that records every call and snapshots the content of the
|
|
// install SOURCE file at call time (the deferred os.Remove would erase it before the test can look).
|
|
type recordingRunner struct {
|
|
calls [][]string
|
|
srcContent []string
|
|
}
|
|
|
|
func (r *recordingRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
|
|
r.calls = append(r.calls, append([]string{name}, args...))
|
|
if name == "install" && len(args) > 0 {
|
|
src := args[len(args)-2]
|
|
b, _ := os.ReadFile(src)
|
|
r.srcContent = append(r.srcContent, string(b))
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (r *recordingRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
|
|
return r.Run(ctx, name, args...)
|
|
}
|
|
|
|
// TestInstallSnippet_RandomTempName is the audit-B1 negative test: the staged install SOURCE must be a
|
|
// RANDOM os.CreateTemp name (felhom-guest-hook-<random>.sh), never the fixed, pre-creatable
|
|
// /tmp/felhom-guest-hook.sh (a local TOCTOU into a root-executed hookscript), and two consecutive
|
|
// installs must stage through DIFFERENT paths.
|
|
func TestInstallSnippet_RandomTempName(t *testing.T) {
|
|
r := &recordingRunner{}
|
|
if err := InstallSnippet(context.Background(), r); err != nil {
|
|
t.Fatalf("InstallSnippet #1: %v", err)
|
|
}
|
|
if err := InstallSnippet(context.Background(), r); err != nil {
|
|
t.Fatalf("InstallSnippet #2: %v", err)
|
|
}
|
|
if len(r.calls) != 2 {
|
|
t.Fatalf("expected 2 install calls, got %d: %v", len(r.calls), r.calls)
|
|
}
|
|
|
|
randomName := regexp.MustCompile(`felhom-guest-hook-[^/\\]+\.sh$`)
|
|
fixedName := regexp.MustCompile(`felhom-guest-hook\.sh$`)
|
|
var srcs []string
|
|
for i, call := range r.calls {
|
|
// install -m 0755 -- <src> <dest>
|
|
if call[0] != "install" || len(call) != 6 {
|
|
t.Fatalf("call %d: unexpected vector %v", i, call)
|
|
}
|
|
src, dest := call[4], call[5]
|
|
if dest != SnippetPath {
|
|
t.Errorf("call %d: dest = %q, want %q", i, dest, SnippetPath)
|
|
}
|
|
if !randomName.MatchString(src) {
|
|
t.Errorf("call %d: source %q does not match the random felhom-guest-hook-*.sh pattern", i, src)
|
|
}
|
|
if fixedName.MatchString(src) {
|
|
t.Errorf("call %d: source %q is the FIXED predictable temp name (B1 TOCTOU)", i, src)
|
|
}
|
|
srcs = append(srcs, src)
|
|
}
|
|
if srcs[0] == srcs[1] {
|
|
t.Errorf("two consecutive installs staged through the SAME source path %q — must be random per call", srcs[0])
|
|
}
|
|
// Non-hollow: the staged file must actually carry the snippet body at install time.
|
|
for i, c := range r.srcContent {
|
|
if c != snippetBody {
|
|
t.Errorf("call %d: staged content is not the snippet body (got %d bytes)", i, len(c))
|
|
}
|
|
}
|
|
// And the temp is cleaned up after.
|
|
for _, src := range srcs {
|
|
if _, err := os.Stat(src); err == nil {
|
|
t.Errorf("staged temp %q left behind (defer os.Remove missing)", src)
|
|
}
|
|
}
|
|
}
|