Forums Bug Reports Thread

State-changing GET spends user credits without CSRF protection (terminal games)

Patrick Bass · Jun 6 · 16 · 1 Locked
[Minor] [Normal Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 6:54pm

Area: Engagement (audit p9) · Surface: GET /games/guess-byte, GET /games/trivia, GET /games/hangman · Dimension: security · Severity: minor

Because credit-spending happens on a plain GET with no anti-CSRF token, an attacker can force a logged-in victim to spend credits by causing their browser to issue the GET (e.g. an `<img src="https://<tenant>/games/trivia">` or a hidden iframe on any page). This is a CSRF-driven resource-drain (OWASP A01). It can only burn the victim's own credits (no privilege gain), so severity is minor, but state mutation on a GET violates safe-method semantics and the project's own 'spend gated by CSRF on POST' discipline used everywhere else.

Evidence

All three GET handlers deduct credits as a side effect of a GET, with no CSRF token (GET routes carry none). e.g. platform/src/Controllers/GameController.php:79-84 (guessTheByte): `if ($cost > 0 && !UserCredit::canAfford(...)) {...} if ($cost > 0) { UserCredit::spend((int) $user['id'], $cost, 'game_play', 'Guess the Byte'); }` — the same spend-on-GET pattern is in trivia() (lines 167-173) and hangman() (lines 261-267). These are registered as GET in routes.php:558-560. A guess.amount of credits is debited the first time a new game session is created on page load.

Suggested fix. Do not spend credits on the GET render. Move the charge + game-session creation to the existing POST handlers (guessTheBytePost/triviaPost/hangmanPost, which already call validateCsrf()), or require an explicit CSRF-protected 'Start game' POST before debiting. The GET should only display the current session state.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:44am

Resolved — fixed and deployed. Commit 059d6bf29b6f, shipped dev-first then to all tenants on 2026-06-06.

Removed the credit-spend and game-session creation from the three GET handlers (guessTheByte/trivia/hangman); they now only render the existing session (or null). Moved the charge + canAfford check + session init into the matching CSRF-protected POST handlers under a no-live-session 'start' branch that redirects back to the GET. Reset and in-progress guess/answer/letter handling preserved. php -l clean.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.