Skip to content

Missing transaction protection in card dealing causes data corruption on failures #2343

Description

@immortal71

Description

The start_game event handler in GameLive.Show lacks transaction protection when dealing cards to players. If a database error occurs partway through the card-dealing loop, the game enters a corrupted state with some players having cards while others have none, making the game unplayable.

Note: While reviewing issue #2335 (which addresses the ArithmeticError from zero players), I realized it does not solve this data integrity problem. Even with proper player validation, database failures during card dealing will still corrupt game state.

Location

File: copi.owasp.org/lib/copi_web/live/game_live/show.ex (lines 76-83)

Vulnerable Code

# Deal cards to players in round-robin fashion
all_cards
|> Enum.with_index()
|> Enum.each(fn {card, i} ->
  Copi.Repo.insert!(%DealtCard{
    card_id: card.id,
    player_id: Enum.at(players, rem(i, player_count)).id
  })
end)

Problem Analysis

Issue 1: No Transaction Wrapping

The card dealing operation is not wrapped in a database transaction. If dealing fails at any point:

  • Already-dealt cards remain in the database
  • Remaining cards are never dealt
  • Game state becomes inconsistent and unplayable

Issue 2: Using insert! Instead of insert

The code uses Repo.insert! which raises an exception on any failure, causing the LiveView to crash. This provides no opportunity for error handling or rollback.

Issue 3: No Rollback Mechanism

Without transaction protection, there is no way to roll back partial operations when errors occur.

Scenarios That Trigger Data Corruption

  1. Database connection timeout - Connection drops after dealing 30 of 52 cards
  2. Disk space exhaustion - Database runs out of space mid-operation
  3. Constraint violation - Unexpected database constraint error
  4. Database deadlock - Concurrent operations cause deadlock
  5. Network interruption - Network issues during dealing

Example Corruption Scenario

Setup: Game with 3 players, 52 cards to deal

Failure Point: Database connection timeout after inserting 30 cards

Result:

  • Player 1: Has 10 cards (round-robin dealt cards 0, 3, 6, 9, ...)
  • Player 2: Has 10 cards (round-robin dealt cards 1, 4, 7, 10, ...)
  • Player 3: Has 10 cards (round-robin dealt cards 2, 5, 8, 11, ...)
  • Remaining 22 cards: Never dealt
  • Game state: Started (started_at is set), but incomplete deck
  • Impact: Game cannot be played, must be manually deleted from database

Reproduction Steps

  1. Start a game with 3+ players
  2. Simulate database failure during card dealing:
    • Use database connection pooling stress test
    • Temporarily restrict database connections
    • Inject a constraint violation mid-loop
  3. Observe partial card dealing with no rollback
  4. Game becomes unplayable with incomplete card distribution

Impact

  • Data Integrity: Games can enter permanently corrupted states
  • User Experience: Players cannot complete corrupted games
  • Manual Cleanup Required: Corrupted games must be manually removed from database
  • No Error Feedback: Users see crash with no explanation
  • Production Risk: Any transient database issue causes permanent data corruption

Related Files

  • lib/copi_web/live/game_live/show.ex (lines 54-101)
  • lib/copi/cornucopia/dealt_card.ex
  • priv/repo/migrations/20210522185023_create_dealt_cards.exs

References

  • ASVS V2.3.3: "Verify that transactions are being used at the business logic level such that either a business logic operation succeeds in its entirety or it is rolled back to the previous correct state."
  • Related issue ArithmeticError when starting game with zero players #2335: Addresses ArithmeticError validation but does not include transaction protection

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions