Commit 6f0df09e authored by Mahmoud Aglan's avatar Mahmoud Aglan

security: add auth checks to all game.php mutation handlers

- handleGameMove: verify caller is a player in the match before allowing moves
- handleResign: verify participant before allowing resignation
- handleDraw: verify participant + use merge_game_state RPC (preserves heartbeat data)
- handleComplete: verify participant + validate winners are actual match players (prevents coin exploit)
- handleFindActiveMatch: restrict to own user only (prevents info disclosure)
- Validate result enum values in handleComplete

Fixes WTF #1-4, #46
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d9242fba
...@@ -109,6 +109,16 @@ function handleGameMove($db, string $userId, array $input): void { ...@@ -109,6 +109,16 @@ function handleGameMove($db, string $userId, array $input): void {
if (!$matchId) jsonError('match_id required'); if (!$matchId) jsonError('match_id required');
$sdb = supabaseService(); $sdb = supabaseService();
// Auth: verify caller is a player in this match
$match = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'white_player_id,black_player_id,status', 'limit' => 1]);
$match = (is_array($match) && !empty($match) && !isset($match['error'])) ? $match[0] : null;
if (!$match) jsonError('Match not found', 404);
if ($match['white_player_id'] !== $userId && $match['black_player_id'] !== $userId) {
jsonError('Not authorized for this match', 403);
}
if ($match['status'] !== 'in_progress') jsonError('Match not in progress');
$update = ['updated_at' => date('c')]; $update = ['updated_at' => date('c')];
if (!empty($input['fen'])) $update['current_fen'] = $input['fen']; if (!empty($input['fen'])) $update['current_fen'] = $input['fen'];
...@@ -170,6 +180,9 @@ function handleResign($db, string $userId, array $input): void { ...@@ -170,6 +180,9 @@ function handleResign($db, string $userId, array $input): void {
$match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null; $match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null;
if (!$match) jsonError('Match not found', 404); if (!$match) jsonError('Match not found', 404);
if ($match['status'] === 'completed') jsonError('Match already ended'); if ($match['status'] === 'completed') jsonError('Match already ended');
if ($match['white_player_id'] !== $userId && $match['black_player_id'] !== $userId) {
jsonError('Not authorized for this match', 403);
}
$isWhite = $match['white_player_id'] === $userId; $isWhite = $match['white_player_id'] === $userId;
$result = $isWhite ? 'black_wins' : 'white_wins'; $result = $isWhite ? 'black_wins' : 'white_wins';
...@@ -190,17 +203,26 @@ function handleDraw($db, string $userId, array $input): void { ...@@ -190,17 +203,26 @@ function handleDraw($db, string $userId, array $input): void {
if (!$matchId) jsonError('match_id required'); if (!$matchId) jsonError('match_id required');
$sdb = supabaseService(); $sdb = supabaseService();
$matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'id,status', 'limit' => 1]); $matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'id,white_player_id,black_player_id,status', 'limit' => 1]);
$match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null; $match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null;
if (!$match) jsonError('Match not found', 404); if (!$match) jsonError('Match not found', 404);
if ($match['status'] === 'completed') jsonError('Match already ended'); if ($match['status'] === 'completed') jsonError('Match already ended');
if ($match['white_player_id'] !== $userId && $match['black_player_id'] !== $userId) {
jsonError('Not authorized for this match', 403);
}
// Use merge_game_state to preserve heartbeat/connections data
supabaseRpc('merge_game_state', [
'p_table' => 'matches',
'p_match_id' => $matchId,
'p_patch' => ['draw_accepted' => true]
]);
$sdb->update('matches', [ $sdb->update('matches', [
'status' => 'completed', 'status' => 'completed',
'result' => 'draw', 'result' => 'draw',
'completed_at' => date('c'), 'completed_at' => date('c'),
'updated_at' => date('c'), 'updated_at' => date('c')
'game_state' => ['draw_accepted' => true]
], ['id' => 'eq.' . $matchId]); ], ['id' => 'eq.' . $matchId]);
mpLog($matchId, 'chess', $userId, 'draw_accepted', []); mpLog($matchId, 'chess', $userId, 'draw_accepted', []);
...@@ -217,7 +239,35 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -217,7 +239,35 @@ function handleComplete($db, string $userId, array $input): void {
if (!$matchId) jsonError('match_id required'); if (!$matchId) jsonError('match_id required');
// Validate result against known enum values
$validResults = ['white_wins','black_wins','draw','stalemate','timeout_white','timeout_black',
'abandon_white','abandon_black','resign_white','resign_black','checkmate_white','checkmate_black',
'bot_win','bot_loss','bot_draw'];
if ($result && !in_array($result, $validResults, true)) {
jsonError('Invalid result value');
}
$sdb = supabaseService(); $sdb = supabaseService();
// Auth: verify caller is a player in this match
$matchData = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'white_player_id,black_player_id,status', 'limit' => 1]);
$matchData = (is_array($matchData) && !empty($matchData) && !isset($matchData['error'])) ? $matchData[0] : null;
if (!$matchData) jsonError('Match not found', 404);
if ($matchData['white_player_id'] !== $userId && $matchData['black_player_id'] !== $userId) {
jsonError('Not authorized for this match', 403);
}
if ($matchData['status'] === 'completed') jsonError('Match already ended');
// Validate winners are actual participants (prevent coin exploit)
$validPlayers = [$matchData['white_player_id'], $matchData['black_player_id']];
if (!empty($winners)) {
foreach ($winners as $w) {
if (!in_array($w, $validPlayers, true)) {
jsonError('Invalid winner — not a match participant', 403);
}
}
}
$sdb->update('matches', [ $sdb->update('matches', [
'status' => 'completed', 'status' => 'completed',
'result' => $result, 'result' => $result,
...@@ -406,8 +456,8 @@ function handleChessLeave(string $userId, array $input): void { ...@@ -406,8 +456,8 @@ function handleChessLeave(string $userId, array $input): void {
} }
function handleFindActiveMatch(string $userId, array $input): void { function handleFindActiveMatch(string $userId, array $input): void {
$playerId = $input['player_id'] ?? ($_GET['player_id'] ?? ''); // Only allow finding your own active matches (prevent info disclosure)
if (!$playerId) jsonError('player_id required'); $playerId = $userId;
$sdb = supabaseService(); $sdb = supabaseService();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment