fix(P02): input validation + injection hardening — 11 vectors (REQ-150)

Critical fixes:
- logs --job: validate ^[A-Za-z0-9_-]+$ + shellQuote (was %q backtick RCE)
- pprof: isLoopback treats empty host as bind-all (was :6060 bypass)
- backup restore: filepath.Rel containment check (was tar-slip via a/../..)
- WebAuthn reg auth deferred to P04 (requires session infra)

High fixes:
- txn rollback/show/apply: validate ^T-[0-9a-f]{16}$ + shellQuote
- nft diff --against: validate txn ID before filepath.Join
- drain stopAlloc: validate allocID ^[A-Za-z0-9_-]+$
- cluster_compat: shellQuote peer dir name
- podman image: shellQuote (was %q backtick injection)
- nft TrustedProbes: net.ParseIP/CIDR validation + split v4/v6 sets
- sudoers: validate --proxmox-user/--proxmox-role ^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$
  fixed path /etc/sudoers.d/orca; shellQuote pveum/useradd; validateSudoers
  checks actual file
- nft country block: validate ^[A-Z]{2}$ (was len==2 only)

New file: internal/cli/validate.go (shared validators + shellQuote)
All 38 Go test packages pass. go vet + gofmt clean.

---ci---
project: orca
phase: 2
milestone: v0.13
status: complete
requirements:
  covered: [150]
---/ci---
This commit is contained in:
Jon Chery
2026-08-07 19:28:01 +00:00
parent b0158c96e9
commit 4b70e31cf4
16 changed files with 459 additions and 60 deletions
+65
View File
@@ -391,6 +391,71 @@ func TestRestoreRejectsTraversalSymlink(t *testing.T) {
}
}
// createCraftedTarballWithFile creates a tar.gz containing a single
// regular file entry with the given (possibly malicious) name. Used to
// test the tar-slip path-traversal guard (F3).
func createCraftedTarballWithFile(path, name, body string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
gz := gzip.NewWriter(f)
defer gz.Close()
tw := tar.NewWriter(gz)
defer tw.Close()
hdr := &tar.Header{
Name: name,
Typeflag: tar.TypeReg,
Mode: 0o644,
Size: int64(len(body)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write([]byte(body)); err != nil {
return err
}
return nil
}
// TestRestoreRejectsTarSlipRegularFile verifies a tarball with a regular
// file entry whose name contains an embedded ".." traversal (e.g.
// "a/../../etc/passwd") is rejected. The old prefix-only check missed
// this pattern; the F3 filepath.Rel containment check catches it.
func TestRestoreRejectsTarSlipRegularFile(t *testing.T) {
dir := t.TempDir()
tarPath := filepath.Join(dir, "slip.tar.gz")
sigPath := tarPath + ".sig"
if err := createCraftedTarballWithFile(tarPath, "a/../../etc/passwd", "pwned"); err != nil {
t.Fatalf("create tarball: %v", err)
}
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 9)
}
mac := hmac.New(sha256.New, key)
data, _ := os.ReadFile(tarPath)
mac.Write(data)
if err := os.WriteFile(sigPath, []byte(hex.EncodeToString(mac.Sum(nil))), 0o600); err != nil {
t.Fatalf("write sig: %v", err)
}
target := filepath.Join(dir, "restore")
os.MkdirAll(target, 0o755)
err := Restore(RestoreOptions{
InputPath: tarPath,
TargetDir: target,
MasterKey: key,
Force: true,
})
if err == nil {
t.Fatal("Restore should reject tar-slip regular file (F3)")
}
if !strings.Contains(err.Error(), "unsafe path") {
t.Errorf("error should mention unsafe path: %v", err)
}
}
// createCraftedTarball creates a tar.gz containing a single symlink
// entry with the given linkname. Used to test symlink validation.
func createCraftedTarball(path, name, linkname string) error {