Commit 03447d76 authored by Mahmoud Aglan's avatar Mahmoud Aglan

security: fix CORS, input sanitization, invite auth, and move secrets to env

- Replace wildcard CORS (Access-Control-Allow-Origin: *) with domain whitelist
  across all 37 API files via shared includes/cors.php
- friends.php: sanitize PostgREST filter inputs (strip special chars from search)
- friends.php: validate UUID format for profile ID lookups
- friends.php: verify user is invite target before accept/decline (domino, ludo, chess)
- config/constants.php: read secrets from .env file or env vars (no more hardcoded keys)
- Add .env to .gitignore

Fixes WTF #5-6, #9-11
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 6f0df09e
......@@ -10,6 +10,7 @@ Thumbs.db
# Sensitive
*.pem
.env
Connections and docs /
# Deps (none for now, but future-proofing)
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../includes/cors.php';
require_once __DIR__ . '/../includes/supabase.php';
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
......@@ -15,9 +15,7 @@ set_error_handler(function($errno, $errstr, $errfile, $errline) {
});
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
header('Cache-Control: no-cache, must-revalidate');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../includes/cors.php';
require_once __DIR__ . '/../includes/supabase.php';
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......@@ -72,6 +70,10 @@ if ($method === 'GET') {
$query = $_GET['query'] ?? '';
if (strlen($query) < 2) jsonError('Query too short');
// Sanitize PostgREST special characters to prevent filter injection
$query = preg_replace('/[^a-zA-Z0-9\x{0600}-\x{06FF}\s_.-]/u', '', $query);
if (strlen($query) < 2) jsonError('Query too short');
$results = $sdb->get('profiles', [
'or' => "(username.ilike.*{$query}*,display_name.ilike.*{$query}*)",
'id' => 'neq.' . $userId,
......@@ -85,7 +87,13 @@ if ($method === 'GET') {
if ($action === 'profiles') {
$ids = $_GET['ids'] ?? '';
if (!$ids) jsonResponse(['profiles' => []]);
$idList = implode(',', array_map('trim', explode(',', $ids)));
// Validate each ID is a valid UUID to prevent filter injection
$rawIds = array_map('trim', explode(',', $ids));
$validIds = array_filter($rawIds, function($id) {
return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $id);
});
if (empty($validIds)) jsonResponse(['profiles' => []]);
$idList = implode(',', $validIds);
$profiles = $sdb->get('profiles', [
'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,level,is_online'
......@@ -512,12 +520,17 @@ if ($method === 'POST') {
$matches = $sdb->get('domino_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,players',
'select' => 'id,players,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
// Verify current user is the invite target
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to accept this invite', 403);
}
$sdb->update('domino_matches', [
'status' => 'in_progress'
], ['id' => 'eq.' . $matchId]);
......@@ -533,12 +546,17 @@ if ($method === 'POST') {
$matches = $sdb->get('ludo_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,players',
'select' => 'id,players,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
// Verify current user is the invite target
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to accept this invite', 403);
}
$sdb->update('ludo_matches', [
'status' => 'in_progress'
], ['id' => 'eq.' . $matchId]);
......@@ -586,16 +604,55 @@ if ($method === 'POST') {
if (!$matchId) jsonError('match_id required');
if ($gameKey === 'domino') {
$matches = $sdb->get('domino_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to decline this invite', 403);
}
$sdb->update('domino_matches', [
'status' => 'completed',
'result' => 'declined'
], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']);
} elseif ($gameKey === 'ludo') {
$matches = $sdb->get('ludo_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to decline this invite', 403);
}
$sdb->update('ludo_matches', [
'status' => 'completed',
'result' => 'declined'
], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']);
} else {
$matches = $sdb->get('matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to decline this invite', 403);
}
$sdb->update('matches', [
'status' => 'completed',
'result' => 'declined'
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
......@@ -4,9 +4,7 @@
* Can be triggered by: admin panel load, external cron, or client on app boot.
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, PATCH, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
......@@ -15,9 +15,7 @@ set_error_handler(function($errno, $errstr, $errfile, $errline) {
});
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PATCH, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
require_once __DIR__ . '/../includes/cors.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
......
<?php
define('SUPABASE_URL', 'https://safe-supabase-kong.caprover.al-arcade.com');
define('SUPABASE_ANON_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84');
define('SUPABASE_SERVICE_KEY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4');
// Load from .env file if present (not committed to git)
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with(trim($line), '#')) continue;
if (strpos($line, '=') === false) continue;
[$k, $v] = explode('=', $line, 2);
putenv(trim($k) . '=' . trim($v));
}
}
define('SUPABASE_URL', getenv('SUPABASE_URL') ?: 'https://safe-supabase-kong.caprover.al-arcade.com');
define('SUPABASE_ANON_KEY', getenv('SUPABASE_ANON_KEY') ?: '');
define('SUPABASE_SERVICE_KEY', getenv('SUPABASE_SERVICE_KEY') ?: '');
define('SUPABASE_REST', SUPABASE_URL . '/rest/v1');
define('SUPABASE_AUTH', SUPABASE_URL . '/auth/v1');
define('SUPABASE_STORAGE', SUPABASE_URL . '/storage/v1');
define('STOCKFISH_API', 'https://stockfishapi.caprover.al-arcade.com');
define('SWISS_API', 'https://swissapi.caprover.al-arcade.com/api/v1');
define('STOCKFISH_API', getenv('STOCKFISH_API') ?: 'https://stockfishapi.caprover.al-arcade.com');
define('SWISS_API', getenv('SWISS_API') ?: 'https://swissapi.caprover.al-arcade.com/api/v1');
<?php
$allowedOrigins = [
'https://el3ab-player.caprover.al-arcade.com',
'https://el3ab.caprover.al-arcade.com',
'http://localhost:3000',
'http://localhost:8080'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
} else {
header('Access-Control-Allow-Origin: https://el3ab-player.caprover.al-arcade.com');
}
header('Access-Control-Allow-Methods: GET, POST, DELETE, PUT, PATCH, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
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