Skip to content

[medium] Agent network hardening: session binding, placeholder upgrade, body cap, token scope #110

Description

@mbertschler

Summary

A cluster of hardening gaps on the HTTP agent surface. The nodes are normally the operator's own machines, but a single compromised node holding the shared token should not be able to impersonate other nodes, hijack another node's in-flight session, redirect dial-back endpoints, or OOM the agent.

Findings

9a — Session not bound to its caller (MEDIUM). agent/sync.golookupSession/takeSession key purely off receiver_run_id, a sequential runs.id; no handler compares the authenticated caller to sess.peerNodeID. Under the shared token, a second peer can drive /plan//verify//close against another node's concurrent same-volume session (inject plan entries, or send /close status=failed to abort a legitimate sync). Fix: bind the session to the caller's authenticated identity; reject mismatches.

9b — peer:// placeholder upgrade from unauthenticated wire input (MEDIUM). store/nodes.go — when a node row holds the peer://<name> placeholder, GetOrCreatePeerNode upgrades it to whatever endpoint the caller presents (InitiatorEndpoint via finishBegin). A peer can bind an arbitrary node-name's dial-back URL to an attacker address. Latent today (no dial-back yet); becomes SSRF/redirect the moment peer-initiated pulls or symmetric dial-back land. Fix: don't auto-upgrade placeholders from wire input — require operator-configured endpoints, or verify identity before upgrade.

9c — No request body size limit (MEDIUM). agent/sync.godecodeJSON wraps req.Body with no http.MaxBytesReader; PlanRequest.Entries is unbounded; agent/serve.go sets ReadHeaderTimeout but no ReadTimeout. A token-holding peer can OOM the agent with one huge /plan body. Fix: http.MaxBytesReader with a generous cap and/or a per-request len(Entries) cap.

9d — Single agent-wide token; node identity unauthenticated, not volume-scoped (MEDIUM, design). cmd/squirrel/agent.go wires one Token; InitiatorNodeName and every OriginNode are self-declared and unbound to the credential, and the token acts on every hosted volume. This undermines the "sourced from a different peer" conflict decision and all provenance attribution, and allows cross-volume action. Fix shape (larger): per-peer tokens (map token → node identity) and/or bind the authenticated identity to the declared node name and volume. May warrant its own design discussion.

Verified clean (no change needed)

Bearer comparison is constant-time (both sides SHA-256'd before subtle.ConstantTimeCompare); TLS dial-pinning enforces a SHA-256 leaf match when a fingerprint is configured; wire path traversal is rejected (validateRelPath); GET endpoints are read-only; JSON decoding is strict (DisallowUnknownFields).

Adversarial audit of offload-v1 (auditor C M-1..M-4).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity / data-integrity finding

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions