File manager - Edit - /home/webapp69.cm.in.th/u69319090028/Shop/app/Services/Security/SecureUploadService.php
Back
<?php /** * Universal Secure File Upload Service * Enforces 7 Security Layers: * 1. Upload Error Code Checking * 2. Strict File Size Validation * 3. MIME Sniffing via finfo (Server-side) * 4. Magic Bytes Binary Signature Verification * 5. Extension Whitelisting & Double-Extension Stripping * 6. Cryptographic Randomized Filename Generation * 7. Path Traversal & Direct Execution Prevention */ require_once __DIR__ . '/../../Helpers/Logger.php'; require_once __DIR__ . '/../../Core/Exceptions/ValidationException.php'; class SecureUploadService { private static array $allowedImageMimes = [ 'image/jpeg' => ['jpg', 'jpeg'], 'image/png' => ['png'], 'image/webp' => ['webp'] ]; /** * Upload Avatar Image */ public static function uploadAvatar(array $file, int $userId): string { $config = self::getConfig(); $maxSize = $config['uploads']['max_size_avatar'] ?? (2 * 1024 * 1024); return self::processImageUpload($file, 'profiles', 'avatar_' . $userId, $maxSize); } /** * Upload Store Logo or Cover */ public static function uploadStoreImage(array $file, int $storeId, string $prefix = 'logo'): string { $config = self::getConfig(); $maxSize = $config['uploads']['max_size_store_image'] ?? (2 * 1024 * 1024); return self::processImageUpload($file, 'stores', 'store_' . $prefix . '_' . $storeId, $maxSize); } /** * Upload Product Image */ public static function uploadProductImage(array $file, int $storeId): string { $config = self::getConfig(); $maxSize = $config['uploads']['max_size_product'] ?? (5 * 1024 * 1024); return self::processImageUpload($file, 'products', 'prod_' . $storeId, $maxSize); } /** * Upload Review Image */ public static function uploadReviewImage(array $file, int $userId): string { $config = self::getConfig(); $maxSize = $config['uploads']['max_size_review'] ?? (5 * 1024 * 1024); return self::processImageUpload($file, 'reviews', 'rev_' . $userId, $maxSize); } /** * Upload Payment Slip */ public static function uploadPaymentSlip(array $file, int $userId): string { $config = self::getConfig(); $maxSize = $config['uploads']['max_size_payment_slip'] ?? (5 * 1024 * 1024); return self::processImageUpload($file, 'slips', 'slip_' . $userId, $maxSize); } /** * Core Secure Processing Pipeline */ public static function processImageUpload(array $file, string $subDir, string $prefix, int $maxSizeBytes): string { // Layer 1: Upload Error Code Checking if (!isset($file['error']) || is_array($file['error'])) { throw new ValidationException(['file' => 'รูปแบบการอัปโหลดไฟล์ไม่ถูกต้อง']); } switch ($file['error']) { case UPLOAD_ERR_OK: break; case UPLOAD_ERR_NO_FILE: throw new ValidationException(['file' => 'กรุณาเลือกไฟล์ที่ต้องการอัปโหลด']); case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $maxMb = round($maxSizeBytes / (1024 * 1024), 1); throw new ValidationException(['file' => "ขนาดไฟล์เกินกำหนด (สูงสุด {$maxMb} MB)"]); default: throw new ValidationException(['file' => 'เกิดข้อผิดพลาดในการอัปโหลดไฟล์']); } // Layer 2: File Size Validation if ($file['size'] > $maxSizeBytes || $file['size'] <= 0) { $maxMb = round($maxSizeBytes / (1024 * 1024), 1); throw new ValidationException(['file' => "ขนาดไฟล์เกินกำหนด (สูงสุด {$maxMb} MB)"]); } // Layer 3: Server-side MIME Sniffing $tmpPath = $file['tmp_name']; if (!is_uploaded_file($tmpPath)) { Logger::security("Attempted upload of non-uploaded file: {$tmpPath}"); throw new ValidationException(['file' => 'ไฟล์ที่อัปโหลดไม่ถูกต้องตามมาตรฐานความปลอดภัย']); } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($tmpPath); if (!array_key_exists($mime, self::$allowedImageMimes)) { Logger::security("Rejected upload with forbidden MIME: {$mime}"); throw new ValidationException(['file' => 'ประเภทไฟล์ไม่ได้รับอนุญาต (รองรับเฉพาะ JPG, PNG, WEBP)']); } // Layer 4: Magic Bytes Verification if (!self::verifyMagicBytes($tmpPath, $mime)) { Logger::security("Magic bytes mismatch for claimed MIME: {$mime}"); throw new ValidationException(['file' => 'โครงสร้างไบนารีของไฟล์ไม่ถูกต้องตามประเภทรูปภาพ']); } // Layer 5: Extension Whitelisting & Double-Extension Stripping $originalExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $validExts = self::$allowedImageMimes[$mime]; $finalExt = in_array($originalExt, $validExts, true) ? $originalExt : $validExts[0]; // Layer 6: Cryptographic Randomized Safe Filename $randomToken = bin2hex(random_bytes(10)); $cleanPrefix = preg_replace('/[^a-zA-Z0-9_-]/', '', $prefix); $safeFilename = sprintf('%s_%s_%s.%s', $cleanPrefix, time(), $randomToken, $finalExt); // Layer 7: Directory Confinement & Path Traversal Prevention $cleanSubDir = preg_replace('/[^a-zA-Z0-9_-]/', '', $subDir); $targetDir = dirname(__DIR__, 3) . '/uploads/' . $cleanSubDir . '/'; if (!is_dir($targetDir)) { @mkdir($targetDir, 0777, true); } @chmod($targetDir, 0777); if (!is_dir($targetDir) || !is_writable($targetDir)) { throw new ValidationException(['file' => 'ระบบไม่มีสิทธิ์เขียนไฟล์ (Permission Denied) กรุณา CHMOD 777 ให้โฟลเดอร์: ' . $targetDir]); } $targetPath = $targetDir . basename($safeFilename); $saved = false; if (is_uploaded_file($tmpPath)) { $saved = @move_uploaded_file($tmpPath, $targetPath); } if (!$saved) { $saved = @copy($tmpPath, $targetPath); } if (!$saved) { $content = @file_get_contents($tmpPath); if ($content !== false) { $saved = @file_put_contents($targetPath, $content) !== false; } } if (!$saved) { $err = error_get_last(); $msg = $err ? $err['message'] : 'Unknown error'; Logger::error("Failed to save uploaded file to: {$targetPath} | Error: {$msg}"); throw new ValidationException(['file' => 'ไม่สามารถบันทึกไฟล์รูปภาพลงในเซิร์ฟเวอร์ได้']); } // Set secure file permissions @chmod($targetPath, 0644); return $safeFilename; } /** * Verify Magic Bytes Signature of Image */ private static function verifyMagicBytes(string $filePath, string $mime): bool { $handle = @fopen($filePath, 'rb'); if (!$handle) { return false; } $header = fread($handle, 12); fclose($handle); if ($header === false || strlen($header) < 4) { return false; } switch ($mime) { case 'image/jpeg': // JPEG starts with FF D8 FF return strncmp($header, "\xFF\xD8\xFF", 3) === 0; case 'image/png': // PNG starts with 89 50 4E 47 0D 0A 1A 0A return strncmp($header, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) === 0; case 'image/webp': // WEBP starts with RIFF and contains WEBP at byte offset 8 return strncmp($header, "RIFF", 4) === 0 && substr($header, 8, 4) === "WEBP"; default: return false; } } /** * Delete file safely */ public static function deleteFile(string $subDir, ?string $filename): bool { if (empty($filename)) { return false; } $cleanSubDir = preg_replace('/[^a-zA-Z0-9_-]/', '', $subDir); $cleanFilename = basename($filename); $filePath = dirname(__DIR__, 3) . '/uploads/' . $cleanSubDir . '/' . $cleanFilename; if (file_exists($filePath) && is_file($filePath)) { return @unlink($filePath); } return false; } private static function getConfig(): array { return file_exists(__DIR__ . '/../../../config/security.php') ? require __DIR__ . '/../../../config/security.php' : []; } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings