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).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 keyattempt:{game}:{env}:{subject}:{key}atInMemoryEphemeralRendezvousStore.cs:472-487). All clients behind one public IP (CGNAT, households, campus NAT) share one subject: a second client submitting a byte-identicalCreateJoinAttemptRequestreceives the first caller's attempt, andCreatere-derives and returns that attempt'sClientPunchCapability(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