Defensive coding: sanitize free text, constrain inputs

Player-authored free text is scrubbed at the API boundary before storage via a
new validation.py: drop control characters and unpaired surrogates, trim
whitespace, and cap length (names 120, other text 2000). Applied to crew/player
names, the character sheet, like/hate answers, techniques, recruit techniques,
the renamed Name, the Gat description, and challenge stakes. Values matched
against stored text (swap offers, face-card assignments) and identifiers/enums
are left untouched so they still compare equal.

Constrained set-role to the two real roles so it can't stash an arbitrary string
in the column. Audited the rest: queries are parameterized by SQLModel (the lone
f-string SQL in database.py is the hardcoded legacy-migration list, no user
input); objective-type and rollback writes already whitelist their keys; no
endpoint accepts a generic (field, value) write.

Tests cover the sanitizer (control chars, surrogates, emoji, caps) and the
create/join HTTP path end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:54:21 -07:00
parent ccb32e7103
commit 122e66b817
9 changed files with 120 additions and 14 deletions

View File

@@ -18,6 +18,7 @@ from pathlib import Path
from . import crud
from . import maintenance
from .database import run_migrations, get_session, engine
from .validation import sanitize_name
from .ws import manager
logger = logging.getLogger(__name__)
@@ -149,7 +150,7 @@ def create_game_route(
crew_name: str = Form(...),
db: Session = Depends(get_session)
):
game = crud.create_game(db, crew_name=crew_name.strip())
game = crud.create_game(db, crew_name=sanitize_name(crew_name))
# admin_key is only revealed here, to the creator; the state endpoint hides it
return {"id": game.id, "admin_key": game.admin_key}
@@ -165,7 +166,7 @@ def join_game_submit(
# First player is automatically the creator
is_creator = len(game.players) == 0
player = crud.add_player(db, game_id, name.strip(), is_creator=is_creator)
player = crud.add_player(db, game_id, sanitize_name(name), is_creator=is_creator)
return {"id": player.id}
@api.post("/game/{game_id}/player/{player_id}/leave")