[FIX] Fix some securiry issues
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
<?php
|
||||
|
||||
class Auth {
|
||||
private const JWT_SECRET = 'ohmj_secret_key_change_in_production';
|
||||
private const JWT_ALGO = 'HS256';
|
||||
private const JWT_EXPIRY = 3600; // 1 hour
|
||||
|
||||
private string $usersFile;
|
||||
private string $jwtSecret;
|
||||
|
||||
public function __construct(string $usersFile = null) {
|
||||
$this->usersFile = $usersFile ?? __DIR__ . '/../config/users.json';
|
||||
|
||||
// Load JWT secret from environment variable
|
||||
$this->jwtSecret = $_ENV['JWT_SECRET'] ?? getenv('JWT_SECRET');
|
||||
if (empty($this->jwtSecret)) {
|
||||
throw new Exception('JWT_SECRET environment variable is not configured');
|
||||
}
|
||||
}
|
||||
|
||||
public function login(string $username, string $password): array {
|
||||
@@ -45,7 +51,7 @@ class Auth {
|
||||
|
||||
// Verify signature
|
||||
$expectedSignature = base64_encode(
|
||||
hash_hmac('sha256', "$header.$payload", self::JWT_SECRET, true)
|
||||
hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true)
|
||||
);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
@@ -101,7 +107,7 @@ class Auth {
|
||||
]));
|
||||
|
||||
$signature = base64_encode(
|
||||
hash_hmac('sha256', "$header.$payload", self::JWT_SECRET, true)
|
||||
hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true)
|
||||
);
|
||||
|
||||
return "$header.$payload.$signature";
|
||||
|
||||
@@ -376,7 +376,7 @@ class ScoreScanner {
|
||||
return $instruments;
|
||||
}
|
||||
|
||||
public function createScore(string $id, string $name, string $compositor): array {
|
||||
public function createScore(string $id, string $name, string $compositor, array $pieces = []): array {
|
||||
$scoreDir = $this->scoresPath . $id;
|
||||
|
||||
if (is_dir($scoreDir)) {
|
||||
@@ -387,7 +387,19 @@ class ScoreScanner {
|
||||
return ['success' => false, 'error' => 'Failed to create directory'];
|
||||
}
|
||||
|
||||
$iniContent = "[info]\nname = $name\ncompositor = $compositor\n\n[pieces]\ncount = 1\n";
|
||||
// Security: Sanitize inputs to prevent INI injection
|
||||
$name = $this->sanitizeIniValue($name);
|
||||
$compositor = $this->sanitizeIniValue($compositor);
|
||||
|
||||
$pieceCount = count($pieces) > 0 ? count($pieces) : 1;
|
||||
$iniContent = "[info]\nname = $name\ncompositor = $compositor\n\n[pieces]\ncount = $pieceCount\n";
|
||||
|
||||
// Add piece names if provided
|
||||
foreach ($pieces as $piece) {
|
||||
$num = $piece['number'];
|
||||
$pieceName = $this->sanitizeIniValue($piece['name']);
|
||||
$iniContent .= "{$num} = $pieceName\n";
|
||||
}
|
||||
|
||||
if (file_put_contents($scoreDir . '/score.ini', $iniContent) === false) {
|
||||
return ['success' => false, 'error' => 'Failed to create score.ini'];
|
||||
@@ -403,6 +415,11 @@ class ScoreScanner {
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitizeIniValue(string $value): string {
|
||||
// Remove newlines and carriage returns to prevent INI injection
|
||||
return str_replace(["\n", "\r", "\x00"], '', $value);
|
||||
}
|
||||
|
||||
public function updateScore(string $scoreId, ?string $name, ?string $compositor): array {
|
||||
$scoreDir = $this->scoresPath . $scoreId;
|
||||
$iniFile = $scoreDir . '/score.ini';
|
||||
@@ -481,6 +498,47 @@ class ScoreScanner {
|
||||
return ['success' => false, 'error' => 'Score not found'];
|
||||
}
|
||||
|
||||
// Security: Validate file upload
|
||||
if (!isset($file['tmp_name']) || !isset($file['name'])) {
|
||||
return ['success' => false, 'error' => 'No file uploaded'];
|
||||
}
|
||||
|
||||
// Security: Check file upload errors
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
return ['success' => false, 'error' => 'Upload error: ' . $file['error']];
|
||||
}
|
||||
|
||||
// Security: Validate MIME type
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if ($mimeType !== 'application/pdf') {
|
||||
return ['success' => false, 'error' => 'Invalid file type. Only PDF files are allowed'];
|
||||
}
|
||||
|
||||
// Security: Validate extension
|
||||
$originalExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
if ($originalExtension !== 'pdf') {
|
||||
return ['success' => false, 'error' => 'Invalid file extension. Only .pdf files are allowed'];
|
||||
}
|
||||
|
||||
// Security: Check magic bytes (PDF starts with %PDF)
|
||||
$handle = fopen($file['tmp_name'], 'rb');
|
||||
if ($handle) {
|
||||
$header = fread($handle, 4);
|
||||
fclose($handle);
|
||||
if ($header !== '%PDF') {
|
||||
return ['success' => false, 'error' => 'Invalid PDF file header'];
|
||||
}
|
||||
}
|
||||
|
||||
// Security: Check file size (max 20MB)
|
||||
$maxSize = 20 * 1024 * 1024; // 20MB
|
||||
if ($file['size'] > $maxSize) {
|
||||
return ['success' => false, 'error' => 'File too large. Maximum size is 20MB'];
|
||||
}
|
||||
|
||||
// Create directory structure: scoreId/piece/instrument/version
|
||||
$targetDir = $scoreDir . '/' . $piece . '/' . $instrument . '/' . $version;
|
||||
|
||||
@@ -532,4 +590,84 @@ class ScoreScanner {
|
||||
|
||||
return ['success' => false, 'error' => 'Failed to move uploaded file'];
|
||||
}
|
||||
|
||||
public function getScoreFiles(string $scoreId): array {
|
||||
$scoreDir = $this->scoresPath . $scoreId;
|
||||
|
||||
if (!is_dir($scoreDir)) {
|
||||
return ['success' => false, 'error' => 'Score not found'];
|
||||
}
|
||||
|
||||
$tree = $this->buildFileTree($scoreDir, $scoreId);
|
||||
|
||||
return ['success' => true, 'files' => $tree];
|
||||
}
|
||||
|
||||
private function buildFileTree(string $dir, string $scoreId, string $relativePath = ''): array {
|
||||
$items = [];
|
||||
$directories = scandir($dir);
|
||||
|
||||
foreach ($directories as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if ($item === 'score.ini') continue;
|
||||
|
||||
$fullPath = $dir . '/' . $item;
|
||||
$itemRelativePath = $relativePath ? $relativePath . '/' . $item : $item;
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
$children = $this->buildFileTree($fullPath, $scoreId, $itemRelativePath);
|
||||
$items[] = [
|
||||
'name' => $item,
|
||||
'path' => $itemRelativePath,
|
||||
'type' => 'folder',
|
||||
'children' => $children
|
||||
];
|
||||
} else {
|
||||
$items[] = [
|
||||
'name' => $item,
|
||||
'path' => $itemRelativePath,
|
||||
'type' => 'file'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function deleteScoreFile(string $scoreId, string $filePath): array {
|
||||
$scoreDir = $this->scoresPath . $scoreId;
|
||||
$fullPath = $scoreDir . '/' . $filePath;
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
return ['success' => false, 'error' => 'File not found'];
|
||||
}
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
return ['success' => false, 'error' => 'Cannot delete directory'];
|
||||
}
|
||||
|
||||
if (unlink($fullPath)) {
|
||||
$this->cleanupEmptyDirectories($scoreDir, dirname($filePath));
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'Failed to delete file'];
|
||||
}
|
||||
|
||||
private function cleanupEmptyDirectories(string $scoreDir, string $dirPath): void {
|
||||
while ($dirPath && $dirPath !== '.') {
|
||||
$fullPath = $scoreDir . '/' . $dirPath;
|
||||
if (!is_dir($fullPath)) break;
|
||||
|
||||
$files = scandir($fullPath);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
if (empty($files)) {
|
||||
rmdir($fullPath);
|
||||
$dirPath = dirname($dirPath);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user