mirror of
https://github.com/myronblair/epictravelexpeditions
synced 2026-07-28 00:43:14 -05:00
Fix security gaps and dead code found in codebase review
- mailer.php: re-enable TLS certificate verification for CyberMail API calls (was CURLOPT_SSL_VERIFYPEER => false, exposing outbound mail traffic to MITM). - .htaccess / api/.htaccess: block direct HTTP access to .git/, db/, and api/config.php via mod_rewrite [F] rules. Confirmed live that /.git/config, /.git/logs/HEAD, and /db/schema.sql were all directly downloadable (200 OK with real content) - .git exposure leaks the repo's embedded GitHub token and full history; schema.sql leaks the DB layout. Also drops the dead "/setup -> setup_password.php" rewrite, since that file was already deleted from production. - index.php: remove the 'download' route, which required a api/download.php that has never existed in this repo - confirmed live that GET /api/download returns a fatal 500. - functions.php / upload.php / testimonials.php: extract the duplicated image-upload validation/move logic (MIME check, size check, UUID rename, move_uploaded_file) into one handleImageUpload() helper used by both upload endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+6
-1
@@ -4,7 +4,12 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+6
-35
@@ -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']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user