Skip to content

Scope player capabilities and add replay-protected session storage#3246

Open
sydseter wants to merge 19 commits into
masterfrom
player-session-management
Open

Scope player capabilities and add replay-protected session storage#3246
sydseter wants to merge 19 commits into
masterfrom
player-session-management

Conversation

@sydseter

@sydseter sydseter commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR fixes the player authorization issue where the card-play API accepted caller-provided game, player, and card identifiers without consistently proving that the browser controlled that player.

Player authorization is now based on an exact {game_id, player_id} capability stored in the player session. Public game watching remains available without authentication.

Changes

  • Added signed, purpose-specific player capabilities containing the game ID and player ID.
  • Limited player capabilities to five minutes.
  • Exchange capabilities through a CSRF-protected POST request instead of placing them in URLs.
  • Return a clean player URL after the exchange.
  • Make each capability single-use by storing only its SHA-256 digest.
  • Support several players in one game and players in different games within the same browser session.
  • Renew the session when a capability is exchanged.
  • Require the exact game and player pair before allowing player actions.
  • Verify that the selected player belongs to the game and that the dealt card belongs to that player.
  • Keep public game pages available without a player session.
  • Apply CSRF protection to card play.

Session and Replay Storage

Three operating modes are supported:

  1. Default single-node mode
  • Player sessions use Phoenix signed and encrypted cookies.
  • Consumed capability digests are stored in local memory.
  • Replay state is lost when the application node restarts.
  1. Erlang cluster mode
  • Enabled when DNS_CLUSTER_QUERY is configured.
  • Nodes use a globally registered replay registry while connected.
  • If the registry cannot be reached, a node falls back to its local registry.
  • Active digests are synchronized after connectivity returns.
  • The same capability can be accepted on both sides of a network partition before synchronization.
  1. PostgreSQL mode
  • Enabled with POSTGRES_SESSION_STORE_ENABLED=true.
  • Browser cookies contain an opaque session ID protected by Phoenix cookie encryption.
  • Session data is encrypted with AES-256-GCM before being written to PostgreSQL.
  • Atomic unique inserts provide replay protection across all nodes using the database.
  • Capability exchange and session use fail when the database is unavailable instead of falling back to memory.

PostgreSQL mode takes precedence over clustered and local replay storage.

Key Separation

  • SECRET_KEY_BASE protects browser cookies and signs player capabilities.
  • COPI_ENCRYPTION_KEY encrypts sensitive session data stored in PostgreSQL.
  • COPI_ENCRYPTION_KEY is not used for browser cookie encryption or capability signing.

Database Changes

Added tables for:

  • Encrypted server-side player sessions.
  • Consumed capability digests and their expiry times.
  • Expiry indexes support cleanup of old records.

One-time PostgreSQL session cleanup at application startup.

  • Introduced a global session_ttl_seconds configuration, defaulting to seven days.
  • Browser-cookie and PostgreSQL session expiry now use the same TTL.
  • Added session_cleanup.ex, started after Copi.Repo.
  • Cleanup only queries PostgreSQL when POSTGRES_SESSION_STORE_ENABLED is enabled.
  • Expired rows are deleted using the existing expires_at database index.
  • Active sessions are retained.
  • Added tests for enabled and disabled cleanup modes.

Security Results

The original BOLA/IDOR issue is addressed:

  • Request identifiers select resources but do not grant access.
  • Authorization is checked using the server-managed player session.
  • A player from one game cannot be combined with another game ID.
  • A player cannot play a card belonging to another player.

Remaining risks are documented in SECURITY.md and ThreatDragonModels/copi.json, including:

  • Theft of an unexpired capability or session cookie.
  • Replay after a restart in local-memory mode.
  • Duplicate exchange during an Erlang cluster partition.
  • PostgreSQL availability and key-management requirements.
  • Existing voting and card-play race conditions.
  • Intentional public viewing by game link.

Testing

Added coverage for:

  • Single-use and concurrent capability consumption.
  • SHA-256 digest storage and expiry.
  • Local replay storage.
  • Cluster registration, fallback, synchronization, and reconnection.
  • PostgreSQL session encryption and restoration.
  • PostgreSQL replay protection after local state loss.
  • Default mode avoiding PostgreSQL session and replay tables.
  • Multiple authorized players in one browser.
  • Public game watching.
  • Unauthorized player and card-play rejection.
  • A complete three-player flow covering capability exchange, game start, dealing, card play, voting, and the next round.

Validated results:

  • Backend: 439 tests, 0 failures
  • Browser: 15 tests, 0 failures
  • Threat Dragon JSON parses successfully.
  • Formatting and whitespace checks pass.
  • Pre-existing Windows LiveView symlink and unused devguide warnings remain unrelated to this change.

AI Tool Disclosure

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: GitHub CoPilot
    • LLMs and versions: GPT-5.6 Sol
    • Prompts:

Review and secure Copi’s player authorization without adding user accounts or preventing public game watching.

A browser must prove control of a player through a signed capability scoped to one exact game and player. The capability must expire after five minutes, be exchanged once through a CSRF-protected POST body, and never appear in the resulting player URL. Store only a SHA-256 digest for replay detection. The player session must support multiple players in the same game and across different games, last up to seven days, and require an exact {game_id, player_id} match for player actions. The server must also verify that the player belongs to the game and that the card belongs to the player.

Support three storage modes: encrypted Phoenix cookie sessions with local in-memory replay protection by default; optional Erlang-cluster replay coordination with local fallback and later synchronization; and optional PostgreSQL-backed sessions and atomic cross-node replay protection enabled through configuration. PostgreSQL mode must take precedence over the other modes. Use SECRET_KEY_BASE for browser cookies and capability signing. Use COPI_ENCRYPTION_KEY only to encrypt sensitive data stored in PostgreSQL.

Keep public game pages accessible without player authorization. Add integration tests for capability exchange, replay rejection, multiple players, unauthorized access, complete gameplay, cluster behavior, and PostgreSQL storage. Update the README, security documentation, and Threat Dragon model with the remaining risks and operating tradeoffs in clear language.

Affirmation

Copilot AI review requested due to automatic review settings July 17, 2026 13:39
@sydseter
sydseter requested review from cw-owasp and rewtd as code owners July 17, 2026 13:39
@qltysh

qltysh Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

All good ✅

Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md
Comment thread copi.owasp.org/SECURITY.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Copi’s player authorization by moving from caller-provided identifiers to short-lived, signed player capabilities that are exchanged into a server-managed session, with replay protection and optional PostgreSQL-backed session storage.

Changes:

  • Introduces signed, 5-minute player capabilities and a CSRF-protected exchange endpoint that stores {game_id, player_id} capabilities in the session (supporting multiple players/games per browser).
  • Adds replay protection for capability exchange via digest tracking (in-memory by default, cluster-coordinated when enabled, and optionally PostgreSQL-backed for cross-node atomic replay prevention).
  • Updates LiveView flows, API authorization checks, documentation, and adds extensive backend + frontend tests covering the new authorization model and storage modes.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ThreatDragonModels/copi.json Updates threat model text to reflect capability-based auth, replay/storage modes, and remaining risks.
copi.owasp.org/test/copi/player_capability_registry_test.exs Adds unit tests for single-use capability consumption, digest storage, and cluster behaviors.
copi.owasp.org/test/copi_web/router_test.exs Verifies old GET exchange route is gone; asserts card-play requires a player session.
copi.owasp.org/test/copi_web/postgres_session_integration_test.exs Adds integration coverage for PostgreSQL session persistence, encryption, replay rejection, and cleanup.
copi.owasp.org/test/copi_web/live/player_live/show_test.exs Updates tests to require session authorization for player pages and adds redirect coverage.
copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs Updates player creation flow to exchange capability and navigate via clean URL.
copi.owasp.org/test/copi_web/live/player_live_test.exs Updates live tests to follow the capability exchange flow after creating a player.
copi.owasp.org/test/copi_web/live/game_live/show_test.exs Ensures resume links show only stored players for the current game.
copi.owasp.org/test/copi_web/gameplay_flow_test.exs Adds end-to-end multi-player flow test + public-watching/unauthorized rejection checks.
copi.owasp.org/test/copi_web/controllers/api_controller_test.exs Adds/updates controller tests for exchange, session semantics, and stricter authorization.
copi.owasp.org/SECURITY.md Documents capability model, replay risks per mode, key separation, and operational guidance.
copi.owasp.org/README.md Documents the three session/replay storage modes and configuration/operational requirements.
copi.owasp.org/priv/repo/migrations/20260717000001_create_security_session_tables.exs Adds PostgreSQL tables/indexes for sessions and capability-consumption digests.
copi.owasp.org/lib/copi/session_record.ex Introduces Ecto schema for server-side sessions.
copi.owasp.org/lib/copi/session_cleanup.ex Adds one-time startup cleanup task for expired PostgreSQL sessions.
copi.owasp.org/lib/copi/release.ex Refactors migration/rollback to use a custom with_repo/2 wrapper with stop timeout.
copi.owasp.org/lib/copi/player_capability_registry.ex Implements replay protection registry with local/cluster/PostgreSQL modes.
copi.owasp.org/lib/copi/player_capability_consumption.ex Adds Ecto schema for consumed capability digests.
copi.owasp.org/lib/copi/application.ex Starts SessionCleanup and PlayerCapabilityRegistry in the supervision tree.
copi.owasp.org/lib/copi_web/session_store.ex Adds Plug.Session.Store implementation with optional PostgreSQL-backed session storage.
copi.owasp.org/lib/copi_web/security_headers.ex Adds Referrer-Policy: no-referrer for browser and live-reload responses.
copi.owasp.org/lib/copi_web/router.ex Moves card-play into CSRF-protected browser JSON pipeline; adds capability exchange route.
copi.owasp.org/lib/copi_web/player_sessions.ex Adds session helpers for storing/authorizing multiple {game_id, player_id} pairs.
copi.owasp.org/lib/copi_web/player_capability.ex Adds signing/verifying logic for purpose-scoped player capabilities.
copi.owasp.org/lib/copi_web/live/player_live/show.html.heex Adjusts client hook attribute to only request clearing when persistence is disabled.
copi.owasp.org/lib/copi_web/live/player_live/show.ex Enforces that the URL-selected player must be present in the browser session.
copi.owasp.org/lib/copi_web/live/player_live/form_component.ex Emits an exchange event instead of navigating directly to a capability-bearing URL.
copi.owasp.org/lib/copi_web/live/game_live/show.html.heex Shows resume links for all stored players for the current game.
copi.owasp.org/lib/copi_web/live/game_live/show.ex Uses session-stored player ids to compute resume players list.
copi.owasp.org/lib/copi_web/endpoint.ex Switches session storage to CopiWeb.SessionStore and aligns cookie TTL to session_ttl_seconds.
copi.owasp.org/lib/copi_web/controllers/api_controller.ex Adds capability exchange endpoint; enforces exact {game_id, player_id} for card play.
copi.owasp.org/fly.toml Enables PostgreSQL session store by default for Fly deployments.
copi.owasp.org/config/runtime.exs Adds runtime config for clustering and PostgreSQL session store toggles.
copi.owasp.org/config/config.exs Adds defaults for cluster/session-store toggles and shared session TTL.
copi.owasp.org/assets/test/app.test.ts Updates frontend tests for CSRF + credentials and new capability exchange hook.
copi.owasp.org/assets/package-lock.json Bumps phoenix_live_view dependency version in assets lockfile.
copi.owasp.org/assets/js/app.js Adds capability exchange hook, adds CSRF + credentials for card play, adjusts session clearing behavior.
Files not reviewed (1)
  • copi.owasp.org/assets/package-lock.json: Generated file

Comment thread copi.owasp.org/lib/copi_web/session_store.ex
Comment thread copi.owasp.org/config/runtime.exs
Comment thread copi.owasp.org/lib/copi/player_capability_registry.ex
Comment thread copi.owasp.org/lib/copi_web/session_store.ex
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sydseter

Copy link
Copy Markdown
Collaborator Author

I closed most of the quality recommendations from CoPilot and qlty as I don’t think they really represent real concerns. It’s pk for md files to have line-length running beyond 120 characters as every editor has something called word wrap. The session cleanup only happens on start-up and won’t cause any db performance issues so the suggestion about moving it to cron is not warranted. The app restarts weekly anyway so this should still be ok.

@sydseter

Copy link
Copy Markdown
Collaborator Author

The coverage failed at 94.93% for the frontend JS tests. Increasing it.

@sydseter

Copy link
Copy Markdown
Collaborator Author

@rewtd, as you voiced the concern about no longer being able to share your hand if you can't continue playing, a share-your-hand button has been added.

This will make it possible to share your hand with someone who can continue instead of keeping the session.

Skjermbilde 2026-07-17 201659

@sydseter

Copy link
Copy Markdown
Collaborator Author

Fully testable from https://copi.owaspcornucopia.org/

Comment thread copi.owasp.org/SECURITY.md
@sydseter

Copy link
Copy Markdown
Collaborator Author

I have also added support for creating multiple players and still be able to recover them. No real benefit other than making it easier and more convenient to test.
IMG_0789

@sydseter

sydseter commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

My thinking here is that the current copi.owasp.org does not follow best practices according to OWASP ASVS. This ensures it does. It's also a step towards creating a more mature app capable of processing sensitive/personal information. Currently, it shouldn't

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants