From bb070a688d0c90a0d87d79f7c7ceeeb40cbc2795 Mon Sep 17 00:00:00 2001 From: ameer Date: Sat, 2 May 2026 17:34:18 +0800 Subject: [PATCH] fix(room): guard against non-dict WS payloads and unhashable answers The first-pass JSON-decode hardening exposed two latent bugs that the fuzz scenario hits as soon as the WS handler stays alive past a bad message: 1) `data.get("type")` is called on whatever `receive_json()` decodes, but valid JSON can be a list/string/number, not just a dict. Reject non-object payloads with a structured bad_message error before dispatch. 2) `submit_answer` did `if answer not in {"A","B","C","D"}` which raises TypeError when the client sends an unhashable answer (e.g. a dict). Add an isinstance(str) guard so any non-string answer falls into the bad_answer branch instead of crashing the handler. 31/31 pytest still passes. Together with the prior commit, the WS handlers now survive the full set of fuzz payloads without dropping the connection. --- app/room.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/room.py b/app/room.py index 09afe24..e870316 100644 --- a/app/room.py +++ b/app/room.py @@ -92,6 +92,12 @@ class RoomManager: except (WebSocketDisconnect, RuntimeError): break continue + if not isinstance(data, dict): + try: + await websocket.send_json({"type": "error", "code": "bad_message", "message": "Message must be a JSON object"}) + except (WebSocketDisconnect, RuntimeError): + break + continue msg_type = data.get("type") if msg_type == "ping": await websocket.send_json({"type": "pong"}) @@ -119,6 +125,12 @@ class RoomManager: except (WebSocketDisconnect, RuntimeError): break continue + if not isinstance(data, dict): + try: + await websocket.send_json({"type": "error", "code": "bad_message", "message": "Message must be a JSON object"}) + except (WebSocketDisconnect, RuntimeError): + break + continue msg_type = data.get("type") if msg_type == "ping": await websocket.send_json({"type": "pong"}) @@ -268,7 +280,7 @@ class RoomManager: qidx = int(question_idx) except (TypeError, ValueError): return {"type": "error", "code": "bad_question", "message": "Invalid question index"} - if answer not in {"A", "B", "C", "D"}: + if not isinstance(answer, str) or answer not in {"A", "B", "C", "D"}: return {"type": "error", "code": "bad_answer", "message": "Answer must be A, B, C, or D"} async with self.locks[sid]: session = await self.get_session(sid)