Surfaced by independent review of PR #25 (#12). Non-blocking; filed as follow-up.
Problem
src/lambdas/shared/session_state.py → make_session_id returns a deterministic transform of the caller's identity:
f"v1:{len(nation_slug)}:{nation_slug}#{user_id}"
Both user_id and nation_slug appear in every authenticated request (and in logs). A user within the same nation who observes a colleague's user_id can reconstruct that colleague's session_id and attempt to call filter_authorized_confirmations against it — a narrow cross-user confirmation-hijack window (requires co-tenancy + observing user_id + timing between record_pending_confirmation and consume_confirmation).
Severity is low given the per-nation co-tenant trust model, but it's real.
Acceptance Criteria
Source: PR #25 review. Relates to epic #18.
Surfaced by independent review of PR #25 (#12). Non-blocking; filed as follow-up.
Problem
src/lambdas/shared/session_state.py→make_session_idreturns a deterministic transform of the caller's identity:Both
user_idandnation_slugappear in every authenticated request (and in logs). A user within the same nation who observes a colleague'suser_idcan reconstruct that colleague'ssession_idand attempt to callfilter_authorized_confirmationsagainst it — a narrow cross-user confirmation-hijack window (requires co-tenancy + observing user_id + timing betweenrecord_pending_confirmationandconsume_confirmation).Severity is low given the per-nation co-tenant trust model, but it's real.
Acceptance Criteria
session_idincludes a cryptographically random, server-held component (per-session nonce minted at login) OR is derived asHMAC(secret, f"{user_id}:{nation_slug}")so it cannot be reconstructed from observable identity valuesuser_id/nation_slugcannot derive theirsession_idSource: PR #25 review. Relates to epic #18.