Files
orca/internal/daemon/pprof.go
T
Jon Chery 4b70e31cf4 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---
2026-08-07 19:28:01 +00:00

88 lines
2.7 KiB
Go

package daemon
import (
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/http/pprof"
"strings"
"time"
)
// isLoopback reports whether the address binds to a loopback interface
// (127.0.0.1, ::1, localhost). REQ-123: pprof must be loopback-only.
//
// An empty host (e.g. ":6060") binds ALL interfaces and is therefore
// treated as NON-loopback (F2: loopback-bypass fix). Only an explicit
// loopback IP or the "localhost" name is accepted.
func isLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
host = strings.TrimSpace(host)
// F2: empty host (":6060") binds all interfaces — reject.
if host == "" {
return false
}
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip != nil {
return ip.IsLoopback()
}
return false
}
// StartPprof starts the pprof HTTP server on addr. REQ-123: pprof is
// unauthenticated and MUST bind to a loopback interface only; this is a
// hard invariant (F2: the --pprof-allow-public override was a phantom flag
// that was never implemented and has been removed — non-loopback binds are
// always refused).
func StartPprof(addr string, log *slog.Logger) (*http.Server, error) {
if addr == "" {
return nil, nil
}
// REQ-123: pprof must bind to loopback only. This is a hard
// invariant; there is no public-bind override.
if !isLoopback(addr) {
log.Error("pprof refuses non-loopback bind",
slog.String("addr", addr),
slog.String("reason", "REQ-123: pprof is unauthenticated; loopback-only is a hard invariant"))
return nil, fmt.Errorf("pprof: refusing non-loopback bind %s (REQ-123; unauthenticated; loopback-only is a hard invariant)", addr)
}
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
server := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
log.Warn("pprof endpoint exposed",
slog.String("addr", addr),
slog.String("warning", "unauthenticated, operator-only — do not expose publicly"))
go func() {
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("pprof server stopped", slog.String("addr", addr), slog.Any("err", err))
}
}()
return server, nil
}