[FIX] Fix some securiry issues

This commit is contained in:
NADAL Jean-Baptiste
2026-02-18 15:27:55 +01:00
parent 3abc6f6371
commit 039cecc4a6
15 changed files with 2179 additions and 200 deletions

View File

@@ -3,9 +3,21 @@
ini_set('upload_max_filesize', '64M');
ini_set('post_max_size', '64M');
// Security headers
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Origin: *');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
// CORS - Restrict to allowed origins
$allowedOrigins = ['http://localhost:5173', 'http://localhost:3000', 'https://ohmj2.free.fr', 'https://partitions.ohmj.fr'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
@@ -14,6 +26,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
// Rate limiting function
function checkRateLimit(string $key, int $maxRequests = 100, int $windowSeconds = 60): bool {
$rateFile = sys_get_temp_dir() . '/rate_' . md5($key) . '.json';
$now = time();
$data = ['count' => 0, 'reset' => $now + $windowSeconds];
if (file_exists($rateFile)) {
$content = file_get_contents($rateFile);
$data = json_decode($content, true) ?? $data;
}
// Reset if window expired
if ($now > $data['reset']) {
$data = ['count' => 0, 'reset' => $now + $windowSeconds];
}
$data['count']++;
file_put_contents($rateFile, json_encode($data));
return $data['count'] <= $maxRequests;
}
// Check rate limit for login endpoint (5 attempts per minute)
if (preg_match('#^login$#', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/')) && $_SERVER['REQUEST_METHOD'] === 'POST') {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (!checkRateLimit('login_' . $ip, 5, 60)) {
http_response_code(429);
echo json_encode(['error' => 'Too many requests. Please try again later.']);
exit;
}
}
// Check general rate limit (500 requests per minute per IP - increased for testing)
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (!checkRateLimit('general_' . $ip, 500, 60)) {
http_response_code(429);
echo json_encode(['error' => 'Rate limit exceeded']);
exit;
}
require_once __DIR__ . '/lib/Auth.php';
require_once __DIR__ . '/lib/ScoreScanner.php';
@@ -45,6 +97,16 @@ $path = preg_replace('#^api/#', '', $path);
if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
$filePath = urldecode($matches[1]);
// Security: Check for null bytes (must use double quotes for escape sequence)
if (strpos($filePath, "\x00") !== false) {
http_response_code(400);
echo json_encode(['error' => 'Invalid file path']);
exit;
}
// Security: Prevent directory traversal
$filePath = str_replace(['../', '..\\', '..'], '', $filePath);
// Check token from header or query parameter
$downloadToken = $_GET['token'] ?? $token;
$downloadToken = urldecode($downloadToken ?? '');
@@ -62,18 +124,36 @@ if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
exit;
}
$fullPath = '/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/' . $filePath;
$basePath = '/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/';
$fullPath = $basePath . $filePath;
if (!file_exists($fullPath) || !is_file($fullPath)) {
// Security: Verify resolved path is within allowed directory
$realBasePath = realpath($basePath);
$realFilePath = realpath($fullPath);
if ($realFilePath === false || strpos($realFilePath, $realBasePath) !== 0) {
http_response_code(403);
echo json_encode(['error' => 'Access denied']);
exit;
}
// Security: Check file extension
if (strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION)) !== 'pdf') {
http_response_code(403);
echo json_encode(['error' => 'Only PDF files are allowed']);
exit;
}
if (!file_exists($realFilePath) || !is_file($realFilePath)) {
http_response_code(404);
echo json_encode(['error' => 'File not found', 'path' => $fullPath]);
echo json_encode(['error' => 'File not found']);
exit;
}
header('Content-Type: application/pdf');
header('Content-Length: ' . filesize($fullPath));
header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"');
readfile($fullPath);
header('Content-Length: ' . filesize($realFilePath));
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
readfile($realFilePath);
exit;
}
@@ -171,6 +251,7 @@ if ($path === 'admin/scores' && $method === 'POST') {
$id = $input['id'] ?? null;
$name = $input['name'] ?? '';
$compositor = $input['compositor'] ?? '';
$pieces = $input['pieces'] ?? [];
// Auto-generate ID if not provided
if (empty($id)) {
@@ -198,7 +279,7 @@ if ($path === 'admin/scores' && $method === 'POST') {
exit;
}
$result = $scanner->createScore($id, $name, $compositor);
$result = $scanner->createScore($id, $name, $compositor, $pieces);
if ($result['success']) {
echo json_encode(['success' => true, 'score' => $result['score']]);
@@ -237,7 +318,11 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE
if ($result['success']) {
echo json_encode(['success' => true]);
} else {
http_response_code(400);
if (strpos($result['error'], 'not found') !== false) {
http_response_code(404);
} else {
http_response_code(400);
}
echo json_encode(['error' => $result['error']]);
}
exit;
@@ -273,6 +358,43 @@ if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method ===
exit;
}
// GET /admin/scores/:id/files - Get all files for a score
if (preg_match('#^admin/scores/(\d+)/files$#', $path, $matches) && $method === 'GET') {
$scoreId = $matches[1];
$result = $scanner->getScoreFiles($scoreId);
if ($result['success']) {
echo json_encode(['success' => true, 'files' => $result['files']]);
} else {
http_response_code(400);
echo json_encode(['error' => $result['error']]);
}
exit;
}
// DELETE /admin/scores/:id/files - Delete a specific file
if (preg_match('#^admin/scores/(\d+)/files$#', $path, $matches) && $method === 'DELETE') {
$scoreId = $matches[1];
$filePath = $_GET['path'] ?? '';
if (empty($filePath)) {
http_response_code(400);
echo json_encode(['error' => 'File path required']);
exit;
}
$result = $scanner->deleteScoreFile($scoreId, $filePath);
if ($result['success']) {
echo json_encode(['success' => true]);
} else {
http_response_code(400);
echo json_encode(['error' => $result['error']]);
}
exit;
}
// 404 Not Found
http_response_code(404);
echo json_encode(['error' => 'Not found']);