Document and mitigate IP-scoped anonymous join-attempt idempotency under CGNAT #34

Open
opened 2026-08-22 23:29:10 +02:00 by Kyuubi · 0 comments
Owner

Parent: #1
Identified by: v1.0.0 pre-release security review (2026-08-22). Severity: minor — documented contract behavior, but with an integrator-facing sharp edge.

Finding

Anonymous join-attempt idempotency is scoped to an HMAC of the caller's public IP (JoinAttemptService.cs:28-35, store key attempt:{game}:{env}:{subject}:{key} at InMemoryEphemeralRendezvousStore.cs:472-487). All clients behind one public IP (CGNAT, households, campus NAT) share one subject: a second client submitting a byte-identical CreateJoinAttemptRequest receives the first caller's attempt, and Create re-derives and returns that attempt's ClientPunchCapability (JoinAttemptService.cs:114-135) — authorizing cancel, outcome reporting, and UDP endpoint binding on someone else's attempt.

The precondition is reproducing the victim's exact caller-chosen 64-char idempotency key, which is unguessable if integrators use random keys — but nothing enforces that: ContractValidation.IsIdempotencyKeyValid (ContractValidation.cs:39-40) accepts any visible ASCII, and the docs (0008-scoped-join-attempts-and-tickets.md, http-v1.md:74-80) define the IP-derived scope without warning about deterministic keys.

Outcome

Make key randomness a stated contract requirement with SDK support (SDK generates cryptographically random keys by default; docs warn that deterministic keys behind shared IPs collide), and/or bind the idempotent replay to an additional caller-held secret so a colliding key returns a rejection rather than another caller's capability.

Acceptance criteria

  • SDK default path never produces predictable idempotency keys.
  • Integration docs state the shared-IP collision consequence explicitly.
  • A test demonstrates two same-IP callers with identical requests cannot obtain each other's punch capability (or documents the accepted residual behavior and its bound).
Parent: #1 Identified by: v1.0.0 pre-release security review (2026-08-22). Severity: minor — documented contract behavior, but with an integrator-facing sharp edge. ## Finding Anonymous join-attempt idempotency is scoped to an HMAC of the caller's public IP (`JoinAttemptService.cs:28-35`, store key `attempt:{game}:{env}:{subject}:{key}` at `InMemoryEphemeralRendezvousStore.cs:472-487`). All clients behind one public IP (CGNAT, households, campus NAT) share one subject: a second client submitting a byte-identical `CreateJoinAttemptRequest` receives the first caller's attempt, and `Create` re-derives and returns that attempt's `ClientPunchCapability` (`JoinAttemptService.cs:114-135`) — authorizing cancel, outcome reporting, and UDP endpoint binding on someone else's attempt. The precondition is reproducing the victim's exact caller-chosen 64-char idempotency key, which is unguessable **if** integrators use random keys — but nothing enforces that: `ContractValidation.IsIdempotencyKeyValid` (`ContractValidation.cs:39-40`) accepts any visible ASCII, and the docs (`0008-scoped-join-attempts-and-tickets.md`, `http-v1.md:74-80`) define the IP-derived scope without warning about deterministic keys. ## Outcome Make key randomness a stated contract requirement with SDK support (SDK generates cryptographically random keys by default; docs warn that deterministic keys behind shared IPs collide), and/or bind the idempotent replay to an additional caller-held secret so a colliding key returns a rejection rather than another caller's capability. ## Acceptance criteria - SDK default path never produces predictable idempotency keys. - Integration docs state the shared-IP collision consequence explicitly. - A test demonstrates two same-IP callers with identical requests cannot obtain each other's punch capability (or documents the accepted residual behavior and its bound).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: HeiKyu/Rendezvous#34