Add CSRF protection, login throttling, and invite-code rate limiting

The admin panel already checked CSRF tokens on destructive actions,
but the player-facing raw HTML forms (create/join/leave/start
session, toggle ready, lobby chat, feedback) had none - cookie
SameSite=Lax blunts classic cross-site auto-submit attacks but isn't
a substitute for real tokens. Adds matching csrf_token()/
isCsrfTokenValid() checks to all of them.

Also adds login_throttling (5 attempts/15 min) to stop unlimited
password guessing, and a per-user rate limiter (10/min) on the
invite-code join endpoint, since invite codes are only 32-bit and
had no protection against brute-forcing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-08-10 21:41:47 +02:00
co-authored by Claude Sonnet 5
parent 335697e520
commit 2913b8d2a2
8 changed files with 67 additions and 6 deletions
+5
View File
@@ -17,6 +17,7 @@
<div class="card-body">
{% if availableGames is not empty %}
<form method="post">
<input type="hidden" name="_token" value="{{ csrf_token('create_session') }}">
<select name="game_id" class="form-select mb-3">
{% for game in availableGames %}
<option value="{{ game.id }}">
@@ -40,6 +41,7 @@
<div class="card-header bg-secondary text-white">Join Session</div>
<div class="card-body">
<form method="post" class="d-flex gap-2">
<input type="hidden" name="_token" value="{{ csrf_token('join_session') }}">
<input type="text" name="invite_code" class="form-control" placeholder="Enter Invite Code" required>
<button type="submit" name="join_session" class="btn btn-primary text-nowrap">Join Session</button>
</form>
@@ -81,6 +83,7 @@
<code>{{ inviteCode }}</code>
{% else %}
<form method="post" class="d-inline">
<input type="hidden" name="_token" value="{{ csrf_token('create_invite_' ~ session.id) }}">
<input type="hidden" name="session_id" value="{{ session.id }}">
<button type="submit" name="create_invite" class="btn btn-sm btn-outline-secondary">Generate Invite</button>
</form>
@@ -91,12 +94,14 @@
{% if session.status.value == 'created' %}
{% if session.players|length >= session.game.numberOfPlayers %}
<form method="post" class="d-inline">
<input type="hidden" name="_token" value="{{ csrf_token('start_session_' ~ session.id) }}">
<input type="hidden" name="session_id" value="{{ session.id }}">
<button type="submit" name="start_session" class="btn btn-sm btn-success">Start Session</button>
</form>
{% endif %}
{% if session.timer == 0 %}
<form method="post" class="d-inline">
<input type="hidden" name="_token" value="{{ csrf_token('leave_session_' ~ session.id) }}">
<input type="hidden" name="session_id" value="{{ session.id }}">
<button type="submit" name="leave_session" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to leave this session?')">Leave Session</button>
</form>