Description
The public POST /p2p-draft-backup HTTP endpoint validates only draft_code and then persists the client-supplied host_peer_id and snapshot_json to SQLite verbatim, echoing both back on GET /p2p-draft-backup/{code}. No size or shape bound is applied. The WebSocket lobby path already bounds the analogous host_peer_id to MAX_TOKEN_LEN (128 bytes, plus control-character rejection) via validate_token, so the same field is bounded on one transport and unbounded on the other.
Steps to reproduce
- Start a
phase-server (or lobby-only) instance.
POST /p2p-draft-backup with a valid 6-char draft_code, a host_peer_id longer than 128 bytes (and/or a multi-hundred-KB snapshot_json, and/or control characters).
GET /p2p-draft-backup/{code}.
Expected behavior
The oversized/control-char host_peer_id is rejected with 400 Bad Request (matching the WS MAX_TOKEN_LEN bound), and snapshot_json is capped to a sane byte ceiling — before the DB write.
Actual behavior
The row is stored verbatim and echoed back to any caller; the only ceiling is axum's default ~2 MB body limit, and the same draft_code row can be overwritten repeatedly.
Impact
Untrusted client input is persisted and re-served without the size/shape bound its sibling WebSocket path already enforces — inconsistent validation across transports, control characters stored, and row-overwrite amplification.
Component
crates/phase-server/src/admin.rs (p2p_backup_store)
crates/phase-server/src/persistence.rs (save_p2p_backup)
crates/lobby-broker/src/validation.rs (validate_token / MAX_TOKEN_LEN — the WS bound this should match)
Description
The public
POST /p2p-draft-backupHTTP endpoint validates onlydraft_codeand then persists the client-suppliedhost_peer_idandsnapshot_jsonto SQLite verbatim, echoing both back onGET /p2p-draft-backup/{code}. No size or shape bound is applied. The WebSocket lobby path already bounds the analogoushost_peer_idtoMAX_TOKEN_LEN(128 bytes, plus control-character rejection) viavalidate_token, so the same field is bounded on one transport and unbounded on the other.Steps to reproduce
phase-server(or lobby-only) instance.POST /p2p-draft-backupwith a valid 6-chardraft_code, ahost_peer_idlonger than 128 bytes (and/or a multi-hundred-KBsnapshot_json, and/or control characters).GET /p2p-draft-backup/{code}.Expected behavior
The oversized/control-char
host_peer_idis rejected with400 Bad Request(matching the WSMAX_TOKEN_LENbound), andsnapshot_jsonis capped to a sane byte ceiling — before the DB write.Actual behavior
The row is stored verbatim and echoed back to any caller; the only ceiling is axum's default ~2 MB body limit, and the same
draft_coderow can be overwritten repeatedly.Impact
Untrusted client input is persisted and re-served without the size/shape bound its sibling WebSocket path already enforces — inconsistent validation across transports, control characters stored, and row-overwrite amplification.
Component
crates/phase-server/src/admin.rs(p2p_backup_store)crates/phase-server/src/persistence.rs(save_p2p_backup)crates/lobby-broker/src/validation.rs(validate_token/MAX_TOKEN_LEN— the WS bound this should match)