fix: require owner verification for P2P backup reads#5365
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the GET /p2p-draft-backup/:code endpoint to require and validate a host_peer_id query parameter, preventing unauthorized access to draft backups, and adds corresponding unit tests. The review feedback suggests consolidating the query parameter struct to avoid duplication and returning 404 Not Found instead of 403 Forbidden on mismatched peer IDs to mitigate a side-channel draft code enumeration vulnerability.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| /// Query params for `GET /p2p-draft-backup/:code`. | ||
| #[derive(Deserialize)] | ||
| pub struct P2pBackupGetQuery { | ||
| pub host_peer_id: String, | ||
| } | ||
|
|
||
| /// GET /p2p-draft-backup/:code — Retrieve a P2P draft backup. | ||
| pub async fn p2p_backup_get( | ||
| State(app_state): State<AppState>, | ||
| Path(code): Path<String>, | ||
| Query(query): Query<P2pBackupGetQuery>, | ||
| ) -> impl IntoResponse { |
There was a problem hiding this comment.
[MEDIUM] Duplicate query parameter structs. Evidence:
crates/phase-server/src/admin.rs:136-147.\n> Why it matters: Defining separate identical structsP2pBackupGetQueryandP2pBackupDeleteQueryintroduces unnecessary code duplication and reduces maintainability.\n> Suggested fix: Consolidate them into a single sharedP2pBackupQuerystruct.
| /// Query params for `GET /p2p-draft-backup/:code`. | |
| #[derive(Deserialize)] | |
| pub struct P2pBackupGetQuery { | |
| pub host_peer_id: String, | |
| } | |
| /// GET /p2p-draft-backup/:code — Retrieve a P2P draft backup. | |
| pub async fn p2p_backup_get( | |
| State(app_state): State<AppState>, | |
| Path(code): Path<String>, | |
| Query(query): Query<P2pBackupGetQuery>, | |
| ) -> impl IntoResponse { | |
| /// Query params for P2P draft backup endpoints. | |
| #[derive(Deserialize)] | |
| pub struct P2pBackupQuery { | |
| pub host_peer_id: String, | |
| } | |
| /// GET /p2p-draft-backup/:code — Retrieve a P2P draft backup. | |
| pub async fn p2p_backup_get( | |
| State(app_state): State<AppState>, | |
| Path(code): Path<String>, | |
| Query(query): Query<P2pBackupQuery>, | |
| ) -> impl IntoResponse { |
| if let Err(reason) = guard_p2p_backup_overwrite(&existing_peer, &query.host_peer_id) { | ||
| return (StatusCode::FORBIDDEN, reason).into_response(); | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Draft code enumeration side-channel. Evidence:
crates/phase-server/src/admin.rs:156.\n> Why it matters: Returning403 Forbiddenfor mismatched peer IDs and404 Not Foundfor non-existent backups allows attackers to easily enumerate active 6-character draft codes.\n> Suggested fix: ReturnStatusCode::NOT_FOUNDinstead ofStatusCode::FORBIDDENto make unauthorized requests indistinguishable from non-existent backups.
| if let Err(reason) = guard_p2p_backup_overwrite(&existing_peer, &query.host_peer_id) { | |
| return (StatusCode::FORBIDDEN, reason).into_response(); | |
| } | |
| if let Err(_) = guard_p2p_backup_overwrite(&existing_peer, &query.host_peer_id) { | |
| return (StatusCode::NOT_FOUND, "No backup found").into_response(); | |
| } |
Require host_peer_id ownership checks on GET /p2p-draft-backup/:code so a guessed draft code alone cannot read backup snapshots. Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed current head: GET backup reads now require matching host_peer_id, mismatch is non-enumerating 404, and boundary tests cover missing, mismatch, and match cases.
Summary
GET /p2p-draft-backup/{code}only required the 6-char draft code, so anyone with/guessing the code could read a host's backup snapshot.host_peer_idon GET and enforce ownership with the existingguard_p2p_backup_overwritecontract.host_peer_id-> 400host_peer_id-> 403host_peer_id-> 200Impact
This removes unauthenticated read access to P2P backup snapshots by draft code alone while preserving host recovery flows.
Risk / tradeoffs
host_peer_idnow receive 400.Test plan
phase-serverHTTP boundary tests for GET ownership checks