Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ThreatDragonModels/copi.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,13 @@
},
{
"id": "477ee678-6f8c-4ce8-a9f7-83ecc5a34f71",
"title": "ATJ: Be aware of data exposure risk! Copi does not support authentication",
"title": "ATJ: Anyone with a game link can watch the game",
"status": "Open",
"severity": "Low",
"type": null,
"eopGameId": "cornucopia",
"description": "What can go wrong?\n\nMark can access resources or services because there is no authentication requirement, or it was mistakenly assumed authentication would be undertaken by some other system or performed in some previous action.\nWe have not implemented Authentication when using Copi, instead we use a secure randomized string to prevent accidental data exposure. Still, an attacker may get hold of such a url by spoofing Copi or other Colleagues in your organization by leveraging various social engineering techniques like establishing a rogue location: https://capec.mitre.org/data/definitions/616.html.",
"mitigation": "What can we do about it?\n\nAs a security measure, you can choose to run copi on a private cluster\nYou should avoid using your own name or the name of a company or project when creating players and games at copi.owasp.org. And remind others not to do so as well. Instead use a pseudonyme and a fake threat model name.",
"description": "What can go wrong?\n\nAnyone with a game link can watch that game. This is intended. Someone may obtain a game link, a short-lived player capability, or a player session cookie through social engineering, logs, a compromised browser, or a compromised network.\n\nWatching a game does not grant player access. Player access requires a signed capability that expires after 5 minutes. If someone steals that capability before it is used, they may exchange it and become that player. A proxy or monitoring service that records POST request bodies may capture the capability.\n\nBy default, Copi remembers used capabilities in the memory of one application node. That record is lost when the node restarts. Clustered nodes normally share the check through one global registry. If the nodes lose contact, each node uses its local record and the same capability may be accepted by more than one node. Synchronizing later cannot undo an exchange that already succeeded.\n\nOptional PostgreSQL storage avoids these replay gaps, but sessions then depend on the database being available. Session records are encrypted, but anyone who obtains both the database contents and COPI_ENCRYPTION_KEY can read them.\n\nA stolen session cookie can be used to act as every player stored in that session until it expires. An individual session cannot be revoked in default cookie mode. PostgreSQL mode allows revocation by deleting the server-side session record.\n\nVoting and card play still have race conditions. Concurrent requests may create duplicate votes or play more than one card in a round because the database does not enforce those limits.",
"mitigation": "What are we going to do about it?\n\nUse HTTPS and do not record capability exchange request bodies in proxies, monitoring systems, or access logs. Protect SECRET_KEY_BASE and COPI_ENCRYPTION_KEY, and rotate the affected key if it is exposed.\n\nUse PostgreSQL session storage when a capability must be accepted only once across several application nodes. Restrict access to the session tables and use verified TLS for the database connection. Without PostgreSQL mode, accept that a restart or cluster partition can allow reuse during the capability's 5-minute lifetime.\n\nDo not use real names or the name of a company or project for games and players. Use pseudonyms and made-up threat model names. Host Copi on a private network when public viewing by link is not acceptable.\n\nVoting integrity remains tracked in issue 2568.",
"modelType": "EOP",
"new": false,
"number": 9,
Expand Down
4 changes: 3 additions & 1 deletion copi.owasp.org/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ WORKDIR /app

# install hex + rebar
RUN mix local.hex --force \
&& mix local.rebar --force
|| mix archive.install github hexpm/hex branch latest --force
RUN mix local.rebar --force \
|| mix local.rebar rebar3 https://github.com/erlang/rebar3/releases/latest/download/rebar3 --force

# set build ENV
ENV MIX_ENV="prod"
Expand Down
18 changes: 18 additions & 0 deletions copi.owasp.org/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).

## Player session and replay storage

Copi uses encrypted cookie sessions and single-node, in-memory capability replay protection by default. This mode does not store player sessions or capability digests in PostgreSQL.

When `DNS_CLUSTER_QUERY` is set, replay registries coordinate through a globally registered Erlang process. A node falls back to its local registry if the global process cannot be reached and synchronizes its active digests when the connection returns. A capability can still be accepted on both sides of a network partition before that synchronization happens.

PostgreSQL-backed player sessions and replay protection are optional:

```bash
POSTGRES_SESSION_STORE_ENABLED=true
```

When enabled, the browser cookie contains an opaque session ID protected by the normal Phoenix cookie encryption. The player session written to the `copi_sessions` table is separately encrypted with `COPI_ENCRYPTION_KEY`. Consumed capability digests are inserted atomically into `player_capability_consumptions`, so nodes using the same database share replay protection. PostgreSQL mode takes precedence over the in-memory and clustered replay registries.

Run the database migrations before enabling this option. Configure every application node with the same setting, `SECRET_KEY_BASE`, and `COPI_ENCRYPTION_KEY`. `COPI_ENCRYPTION_KEY` is used for session encryption only when state is written to or read from PostgreSQL; browser cookies continue to use Phoenix's `SECRET_KEY_BASE`. Protect database connections with verified TLS when the database is not on a trusted local network. If PostgreSQL is unavailable, session loading and capability exchange fail rather than falling back to a weaker store.

Changing the storage mode invalidates existing browser sessions. Capability exchange tokens remain valid for 5 minutes and player sessions remain valid for up to 7 days in every mode.

## More about Phoenix

* Official website: [https://www.phoenixframework.org/](https://www.phoenixframework.org/)
Expand Down
55 changes: 44 additions & 11 deletions copi.owasp.org/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,30 @@ The RateLimiter logs configuration on startup and warnings when limits are excee
- Adjust rate limits based on legitimate usage patterns
- Identify problematic IPs

## Player Capabilities

Anyone with a game link can watch that game. Watching does not require a player session.

Player actions have a separate check. When a player is created, Copi signs a capability that contains the game ID, the player ID, and the `player` purpose. The capability expires after 5 minutes. The browser sends it in the body of a CSRF-protected POST request. The endpoint adds the player to the encrypted session cookie and returns the normal player URL without the capability.
Comment thread
sydseter marked this conversation as resolved.

Each capability is intended for one exchange. Copi stores only a SHA-256 digest for 5 minutes. The check and update are one atomic operation in each replay store. The bearer value is not stored in a replay registry.
Comment thread
sydseter marked this conversation as resolved.

By default, replay protection uses memory on one application node. When `DNS_CLUSTER_QUERY` enables Erlang clustering, each node first uses one globally registered registry. If it cannot reach that registry, it uses its local registry and synchronizes active digests after the connection returns. This keeps capability exchange available during a cluster outage, but the same capability may be accepted independently on both sides of a partition before synchronization. Synchronization cannot undo an exchange that already succeeded. In-memory entries are also lost when a node restarts.
Comment thread
sydseter marked this conversation as resolved.

PostgreSQL storage can be enabled with `POSTGRES_SESSION_STORE_ENABLED=true`. In that mode, a unique database insert provides replay protection across all nodes that use the same database. PostgreSQL replay protection takes precedence over the local and clustered registries. If the database is unavailable, the exchange fails instead of falling back to memory.
Comment thread
sydseter marked this conversation as resolved.

The player session lasts for up to 7 days. By default, it is stored in the signed and encrypted Phoenix session cookie. With PostgreSQL storage enabled, the cookie contains an opaque session ID protected by Phoenix cookie encryption, while session data written to the `copi_sessions` table is encrypted with `COPI_ENCRYPTION_KEY` using AES-256-GCM. `COPI_ENCRYPTION_KEY` is not used for browser cookie encryption or capability signing. One session can contain several player capabilities for the same game and for different games. Adding a player does not remove existing players. The cookie is HTTP-only, uses `SameSite=Lax`, and is marked secure in production.
Comment thread
sydseter marked this conversation as resolved.

The game ID and player ID in the URL select the active player. Copi accepts the action only when that exact game and player pair is present in the cookie. A player ID from another stored game cannot be combined with the current game ID. Copi also checks that the card belongs to the selected player. Card play uses CSRF protection.
Comment thread
sydseter marked this conversation as resolved.

A game link and a player capability are not interchangeable. A game link allows someone to watch a game. It does not allow that person to play a card as another player.
Comment thread
sydseter marked this conversation as resolved.

The player capability is a bearer secret until it is exchanged or expires. Sending it in a POST body keeps it out of the address bar, browser history, referrer headers, and normal URL logs. The exchange response uses `Cache-Control: no-store`. Use HTTPS and make sure proxies and application monitoring do not log request bodies containing capabilities.
Comment thread
sydseter marked this conversation as resolved.

Cluster mode improves replay protection while nodes are connected, but it is not a consensus or durable store. A network partition, global registry timeout, node restart, or cluster restart can allow reuse during the 5-minute capability lifetime. PostgreSQL mode avoids those replay gaps when every node uses the same available database, but it adds session availability and confidentiality dependence on PostgreSQL. Use verified database TLS and restrict access to the session tables.
Comment thread
sydseter marked this conversation as resolved.

The encrypted session cookie is also a bearer credential. In default cookie mode, a stolen copy can be used until it expires unless the signing key is rotated. In PostgreSQL mode, a copied cookie refers to the same server-side row and can be invalidated by deleting that row, but it remains usable until expiry if no revocation occurs. The cookie is not tied to an IP address because client addresses change and may be shared or supplied through an untrusted proxy header. A compromised browser or script running in the Copi origin may still perform actions using an HTTP-only cookie even though it cannot read the cookie value.
Comment thread
sydseter marked this conversation as resolved.

## Encryption Key Setup

Game and player names are encrypted at the application level using AES-256-GCM.
Expand Down Expand Up @@ -159,26 +183,35 @@ Please also read [SECURITY.md](SECURITY.md) to ensure you have taken the appropr

Here is a short summary of what you need to be aware of:

### ATJ: Mark can access resources or services because there is no authentication requirement, or it was mistakenly assumed that authentication would be undertaken by some other system or performed in some previous action.
### ATJ: Anyone with a game link can watch the game

#### What can go wrong?

Be aware of data exposure risk! Copi does not support authentication.
We have not implemented Authentication when using Copi, instead we use a secure randomized string to prevent accidental data exposure. Still, an attacker may get hold of such a url by spoofing Copi or other Colleagues in your organization by leveraging various social engineering techniques like establishing a rogue location: [https://capec.mitre.org/data/definitions/616.html](https://capec.mitre.org/data/definitions/616.html).
Copi does not use user accounts. Anyone with a game link can watch that game. This is intended.
Comment thread
sydseter marked this conversation as resolved.

Someone may obtain a game link, a short-lived player capability, or a player session cookie through social engineering, logs, a compromised browser, or a compromised network. See [CAPEC-616](https://capec.mitre.org/data/definitions/616.html) and [CAPEC-569](https://capec.mitre.org/data/definitions/569.html).
Comment thread
sydseter marked this conversation as resolved.

Watching a game does not grant player access. Player access requires a signed capability that expires after 5 minutes. If someone steals that capability before it is used, they may exchange it and become that player. The capability is sent in a POST body, so a proxy or monitoring service that records request bodies may capture it.
Comment thread
sydseter marked this conversation as resolved.

An attacker could use various tools for capturing logs or http requests which may lead to information disclosure if your participants' network has been comporised: [https://capec.mitre.org/data/definitions/569.html](https://capec.mitre.org/data/definitions/569.html).
By default, Copi remembers used capabilities in the memory of one application node. That record is lost when the node restarts. In a cluster, nodes normally share this check through one global registry. If the nodes lose contact, each node uses its own local record. The same capability may then be accepted by more than one node. Synchronizing the records later cannot undo an exchange that has already succeeded.
Comment thread
sydseter marked this conversation as resolved.

Do you think this is strange? Indeed, in this day and age, it is, but if we were to implement authentication, we would also have to process more personal information, which would open us up to more threats. We could indeed mitigate those threats, but we would rather remain privacy-friendly and process as little personal information as possible.
An optional PostgreSQL mode avoids these replay gaps by keeping the session and replay records in one database. This makes player sessions depend on the database being available. The session records are encrypted, but anyone who obtains both the database contents and `COPI_ENCRYPTION_KEY` can read them.
Comment thread
sydseter marked this conversation as resolved.

Game integrity is still enforceable only by client behavior in a few important paths. During the game, the app only checks that a voted card belongs to the same game before inserting a vote, so a crafted client can self-vote because there is no server-side owner check. The app separately creates the votes table without a uniqueness constraint, and the dealt_cards table stores played_in_round without any DB-level rule that limits a player to one card per round. A malicious or racing client can therefore duplicate votes or play multiple cards in the same round.
The player session cookie is a bearer credential. Anyone who steals a valid copy can act as every player stored in that session until it expires. In the default cookie mode, an individual stolen session cannot be revoked. In PostgreSQL mode, deleting the server-side session record revokes it.
Comment thread
sydseter marked this conversation as resolved.

Voting and card play still have race conditions. The votes table has no uniqueness constraint, so two requests made at nearly the same time may create duplicate votes. The database also has no rule that limits a player to one played card per round, so concurrent requests may play more than one card.
Comment thread
sydseter marked this conversation as resolved.

#### What are we going to do about it?

We are not working towards implementing authentication in Copi. Instead, we are utilizing magic links. Arguable this is not authentication, but it's worth noting that your threat model is not stored on copi.owasp.org, just your game and the cards you voted on. For a threat actor to be able to piece together this information and use it against you, given that he gets hold of the magic link, you would have to use your full name and add the URL to your project in the game name field when creating the game. We are working towards informing users that they should under no circumstances do this kind of thing, but even in the case that you still do. The cards themselves are too generic and don't contain the sensitive discussions that you had during your game.
As a security measure, you can choose to run Copi on a private cluster
You should avoid using your own name or the name of a company or project when creating players and games at copi.owasp.org. And remind others not to do so as well. Instead, use a pseudonym and a fake threat model name.
Use HTTPS. Do not record capability exchange request bodies in proxies, application monitoring, or access logs. Protect `SECRET_KEY_BASE` and `COPI_ENCRYPTION_KEY`, and rotate the affected key if it is exposed.
Comment thread
sydseter marked this conversation as resolved.

Use PostgreSQL session storage when a capability must be accepted only once across several application nodes. Restrict access to the session tables and use verified TLS for the database connection. Without PostgreSQL mode, accept that a restart or cluster partition can allow a capability to be reused during its 5-minute lifetime.

Copi stores game and card choices, but not the discussion held by the players. Do not use a real person, company, or project name for a game or player. Use a pseudonym and a made-up threat model name.
Comment thread
sydseter marked this conversation as resolved.

Run Copi on a private network if viewing by game link is not suitable for your use case.
Comment thread
sydseter marked this conversation as resolved.

There is a GitHub issue to resolve the voting integrity vulnerability (see: https://github.com/OWASP/cornucopia/issues/2568). The damage is limited by the fact that most players during a game know each other and by having the url to the game being a random magic link.
Voting integrity is tracked in [issue 2568](https://github.com/OWASP/cornucopia/issues/2568).

## CR6: Romain can read and modify unencrypted data in memory or in transit (e.g., cryptographic secrets, credentials, session identifiers, personal and commercially-sensitive data), in use or in communications within the application, or between the application and users, or between the application and external systems.

Expand Down Expand Up @@ -209,7 +242,7 @@ The Fly.io reverse proxy does not strip or rewrite untrusted X-Forwarded-For hea

We are working on minimizing the probability of functionality misuse by implementing rate limiting on the creation of games and players (see: [issues/1877](https://github.com/OWASP/cornucopia/issues/1877)). Once that is taken care of, you should be able to configure these limits to prevent DoS attacks when hosting Copi yourself. It's vital that you limit the number of sockets the application accepts concurrently. On fly.io that is done in the following way: [fly.toml](https://github.com/OWASP/cornucopia/blob/fb9aae62531dde8db154729d0df4aa28a3400063/copi.owasp.org/fly.toml#L27) A 30 socket limit for Copi should allow you to handle 20.000 requests per min if you have 2 single cpu nodes Which we have tested against that setup.

Use of Fly-Client-IP has been considered. We are looking into implementing this for Copi (see: https://github.com/OWASP/cornucopia/issues/3227).
When Copi receives traffic directly from Fly Proxy, set `USE_FLY_CLIENT_IP=true`. [Fly.io documents `Fly-Client-IP`](https://fly.io/docs/networking/request-headers/#fly-client-ip) as the client IP address from Fly Proxy's perspective and says it may be a better choice than `X-Forwarded-For`, which must be treated with caution to avoid spoofing. Using it prevents Copi's rate limiter from trusting a client-controlled, left-most `X-Forwarded-For` value. If another reverse proxy is in front of Fly.io, `Fly-Client-IP` contains that proxy's address instead of the original client's address; in that deployment, parse `X-Forwarded-For` using an explicit trusted-proxy configuration.
Comment thread
sydseter marked this conversation as resolved.

### CK: Grant can utilize the application to deny service to some or all of its users

Expand Down
Loading
Loading