diff --git a/.htaccess b/.htaccess index a05d9d0..5b543ec 100644 --- a/.htaccess +++ b/.htaccess @@ -13,13 +13,16 @@ DirectoryIndex index.html index.php RewriteEngine On RewriteBase / +# Block access to version control metadata and the raw DB schema dump. +# These were previously reachable directly over HTTP (e.g. /.git/config, +# /db/schema.sql) and leaked repo/DB internals - deny them outright. +RewriteRule ^\.git(/|$) - [F,L] +RewriteRule ^db/ - [F,L] + # API Routes - Direct to PHP backend RewriteCond %{REQUEST_URI} ^/api/ RewriteRule ^api/(.*)$ api/api/$1 [L,QSA] -# Admin setup page -RewriteRule ^setup/?$ setup_password.php [L] - # Static files and directories - serve directly RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d diff --git a/api/.htaccess b/api/.htaccess index 5a306c7..84e679f 100644 --- a/api/.htaccess +++ b/api/.htaccess @@ -4,7 +4,12 @@ RewriteEngine On RewriteBase /api/ - + + # Block direct access to config.php / .env - this was reachable at + # /api/config.php in production (200 OK) despite the FilesMatch/Require + # rule below, which LiteSpeed appears not to honor from .htaccess here. + RewriteRule ^(config\.php|\.env)$ - [F,L] + # Route all requests to index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d diff --git a/api/api/testimonials.php b/api/api/testimonials.php index 991d3b3..4815440 100644 --- a/api/api/testimonials.php +++ b/api/api/testimonials.php @@ -77,19 +77,9 @@ if ($method === 'DELETE' && $id) { // POST upload testimonial image (public) if ($method === "POST" && $id === "upload") { if (!isset($_FILES["file"])) jsonResponse(["error" => "No file uploaded"], 400); - $file = $_FILES["file"]; - if ($file["error"] !== UPLOAD_ERR_OK) jsonResponse(["error" => "Upload error"], 400); - if ($file["size"] > MAX_UPLOAD_SIZE) jsonResponse(["error" => "File too large (max 5MB)"], 400); - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime = finfo_file($finfo, $file["tmp_name"]); - finfo_close($finfo); - $allowed = ["image/jpeg","image/jpg","image/png","image/webp"]; - if (!in_array($mime, $allowed)) jsonResponse(["error" => "Invalid file type"], 400); - $ext = strtolower(pathinfo($file["name"], PATHINFO_EXTENSION)); - $name = generateUuid() . "." . $ext; - $dest = UPLOAD_DIR . $name; - if (!move_uploaded_file($file["tmp_name"], $dest)) jsonResponse(["error" => "Failed to save"], 500); - jsonResponse(["url" => "/api/uploads/" . $name]); + $result = handleImageUpload($_FILES["file"]); + if (isset($result['error'])) jsonResponse(["error" => $result['error']], $result['status']); + jsonResponse(["url" => $result['url']]); } jsonResponse(['error' => 'Invalid testimonials endpoint'], 404); diff --git a/api/api/upload.php b/api/api/upload.php index 38d6f08..d2040c4 100644 --- a/api/api/upload.php +++ b/api/api/upload.php @@ -10,43 +10,14 @@ if ($method === 'POST' && $id === 'image') { jsonResponse(['error' => 'No file uploaded'], 400); } - $file = $_FILES['file']; - - // Validate file - if ($file['error'] !== UPLOAD_ERR_OK) { - jsonResponse(['error' => 'File upload failed'], 400); + $result = handleImageUpload($_FILES['file']); + if (isset($result['error'])) { + jsonResponse(['error' => $result['error']], $result['status']); } - - // Check file size - if ($file['size'] > MAX_UPLOAD_SIZE) { - jsonResponse(['error' => 'File too large. Maximum size is 5MB'], 400); - } - - // Check file type - $allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mimeType = finfo_file($finfo, $file['tmp_name']); - finfo_close($finfo); - - if (!in_array($mimeType, $allowedTypes)) { - jsonResponse(['error' => 'Invalid file type. Only JPG, PNG, and WebP allowed'], 400); - } - - // Generate unique filename - $extension = pathinfo($file['name'], PATHINFO_EXTENSION); - $filename = generateUuid() . '.' . $extension; - $filepath = UPLOAD_DIR . $filename; - - // Move uploaded file - if (!move_uploaded_file($file['tmp_name'], $filepath)) { - jsonResponse(['error' => 'Failed to save file'], 500); - } - - $fileUrl = '/api/uploads/' . $filename; - + jsonResponse([ - 'url' => $fileUrl, - 'filename' => $filename + 'url' => $result['url'], + 'filename' => $result['filename'] ]); } diff --git a/api/includes/functions.php b/api/includes/functions.php index 307c504..6573e1e 100644 --- a/api/includes/functions.php +++ b/api/includes/functions.php @@ -85,3 +85,40 @@ function validateRequired($data, $requiredFields) { return $errors; } + +/** + * Validate an uploaded image file ($_FILES['file']) and move it into + * UPLOAD_DIR under a fresh UUID filename. Shared by all endpoints that + * accept image uploads (was previously duplicated per-endpoint). + * + * Returns ['url' => ..., 'filename' => ...] on success, or + * ['error' => ..., 'status' => ...] on failure. + */ +function handleImageUpload($file) { + if (!$file || $file['error'] !== UPLOAD_ERR_OK) { + return ['error' => 'File upload failed', 'status' => 400]; + } + + if ($file['size'] > MAX_UPLOAD_SIZE) { + return ['error' => 'File too large. Maximum size is 5MB', 'status' => 400]; + } + + $allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (!in_array($mimeType, $allowedTypes)) { + return ['error' => 'Invalid file type. Only JPG, PNG, and WebP allowed', 'status' => 400]; + } + + $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $filename = generateUuid() . '.' . $extension; + $filepath = UPLOAD_DIR . $filename; + + if (!move_uploaded_file($file['tmp_name'], $filepath)) { + return ['error' => 'Failed to save file', 'status' => 500]; + } + + return ['url' => '/api/uploads/' . $filename, 'filename' => $filename]; +} diff --git a/api/includes/mailer.php b/api/includes/mailer.php index 3af8dc2..8495a0a 100644 --- a/api/includes/mailer.php +++ b/api/includes/mailer.php @@ -28,7 +28,8 @@ function sendgridSend(string $toEmail, string $toName, string $subject, string $ 'Content-Type: application/json', ], CURLOPT_TIMEOUT => 20, - CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, ]); $response = curl_exec($ch); diff --git a/api/index.php b/api/index.php index de2d080..9437031 100644 --- a/api/index.php +++ b/api/index.php @@ -58,11 +58,7 @@ try { case 'upload': require __DIR__ . '/api/upload.php'; break; - - case 'download': - require __DIR__ . '/api/download.php'; - break; - + case 'testimonials': require __DIR__ . '/api/testimonials.php'; break;