<?php
class SecureFileUpload {
private $uploadDir;
private $maxFileSize = 5 * 1024 * 1024; // 5MB
private $allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/pdf' => 'pdf'
];
public function __construct() {
// Store uploads outside web root
$this->uploadDir = '/var/app_data/uploads/';
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function handleUpload() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['upload'])) {
return ['success' => false, 'message' => 'No file uploaded'];
}
$file = $_FILES['upload'];
try {
$this->validateFile($file);
$safeFileName = $this->sanitizeAndStore($file);
return [
'success' => true,
'message' => 'File uploaded successfully',
'fileId' => $safeFileName
];
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
private function validateFile($file) {
// Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('Upload error: ' . $file['error']);
}
// Check file size
if ($file['size'] > $this->maxFileSize) {
throw new Exception('File too large. Maximum size: ' . ($this->maxFileSize / 1024 / 1024) . 'MB');
}
// Verify actual MIME type using file content
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!array_key_exists($mimeType, $this->allowedTypes)) {
throw new Exception('File type not allowed: ' . $mimeType);
}
// Additional validation for images
if (strpos($mimeType, 'image/') === 0) {
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
throw new Exception('Invalid image file');
}
}
// Check for embedded PHP code in file content
$content = file_get_contents($file['tmp_name']);
if (preg_match('/<\?php|<\?=|<script/i', $content)) {
throw new Exception('Potentially malicious content detected');
}
}
private function sanitizeAndStore($file) {
// Generate safe filename
$extension = $this->allowedTypes[mime_content_type($file['tmp_name'])];
$safeFileName = uniqid('upload_', true) . '.' . $extension;
$destination = $this->uploadDir . $safeFileName;
// Move file
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new Exception('Failed to store file');
}
// Set secure permissions
chmod($destination, 0644);
return $safeFileName;
}
// Secure file serving
public function serveFile($fileId) {
$filePath = $this->uploadDir . basename($fileId); // Prevent path traversal
if (!file_exists($filePath)) {
http_response_code(404);
exit('File not found');
}
// Verify file is in allowed directory
$realPath = realpath($filePath);
$realUploadDir = realpath($this->uploadDir);
if (strpos($realPath, $realUploadDir) !== 0) {
http_response_code(403);
exit('Access denied');
}
// Set secure headers
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($fileId) . '"');
header('X-Content-Type-Options: nosniff');
readfile($filePath);
}
}
// Usage
$uploader = new SecureFileUpload();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $uploader->handleUpload();
echo json_encode($result);
exit;
}
?>
<html>
<body>
<h2>Secure File Upload</h2>
<form method="post" enctype="multipart/form-data">
<p>Allowed files: Images (JPG, PNG, GIF) and PDF documents only</p>
<p>Maximum size: 5MB</p>
<input type="file" name="upload" accept=".jpg,.jpeg,.png,.gif,.pdf" required>
<input type="submit" value="Upload File">
</form>
</body>
</html>