1. php
  2. /security
  3. /secure-file-uploads

Secure File Uploads in PHP

Introduction to Secure File Uploads

File uploads are a common feature in web applications but pose significant security risks if not implemented properly. Attackers can exploit insecure file upload mechanisms to execute malicious code, compromise servers, or launch various attacks. This guide covers best practices for implementing secure file upload functionality in PHP.

Common File Upload Vulnerabilities

1. Remote Code Execution

Uploading executable files (PHP, ASP, JSP) that can be executed by the server.

2. Path Traversal

Using directory traversal sequences (../) to write files outside intended directories.

3. File Type Spoofing

Bypassing file type restrictions by manipulating file extensions or MIME types.

4. Denial of Service

Uploading large files to consume server resources.

5. Cross-Site Scripting (XSS)

Uploading HTML/SVG files containing malicious scripts.

Basic Secure Upload Implementation

File Upload Class

<?php
class SecureFileUpload
{
    private $uploadDir;
    private $allowedTypes = [];
    private $allowedExtensions = [];
    private $maxFileSize;
    private $maxFiles;
    
    public function __construct(string $uploadDir, int $maxFileSize = 5242880) // 5MB default
    {
        $this->uploadDir = rtrim($uploadDir, '/');
        $this->maxFileSize = $maxFileSize;
        $this->maxFiles = 10; // Default max files per upload
        
        // Create upload directory if it doesn't exist
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
        
        // Ensure directory is not web accessible
        $this->protectUploadDirectory();
    }
    
    public function setAllowedTypes(array $types): self
    {
        $this->allowedTypes = $types;
        return $this;
    }
    
    public function setAllowedExtensions(array $extensions): self
    {
        $this->allowedExtensions = array_map('strtolower', $extensions);
        return $this;
    }
    
    public function setMaxFileSize(int $size): self
    {
        $this->maxFileSize = $size;
        return $this;
    }
    
    public function setMaxFiles(int $count): self
    {
        $this->maxFiles = $count;
        return $this;
    }
    
    public function upload(array $files): array
    {
        $results = [];
        $uploadedFiles = $this->normalizeFilesArray($files);
        
        if (count($uploadedFiles) > $this->maxFiles) {
            throw new Exception("Too many files. Maximum {$this->maxFiles} allowed.");
        }
        
        foreach ($uploadedFiles as $file) {
            try {
                $result = $this->processSingleFile($file);
                $results[] = $result;
            } catch (Exception $e) {
                $results[] = [
                    'success' => false,
                    'error' => $e->getMessage(),
                    'original_name' => $file['name'] ?? 'unknown'
                ];
            }
        }
        
        return $results;
    }
    
    private function processSingleFile(array $file): array
    {
        // Basic validation
        $this->validateUpload($file);
        
        // Security checks
        $this->performSecurityChecks($file);
        
        // Generate secure filename
        $secureFileName = $this->generateSecureFileName($file['name']);
        
        // Move file to secure location
        $destination = $this->uploadDir . '/' . $secureFileName;
        
        if (!move_uploaded_file($file['tmp_name'], $destination)) {
            throw new Exception('Failed to move uploaded file');
        }
        
        // Set secure permissions
        chmod($destination, 0644);
        
        return [
            'success' => true,
            'original_name' => $file['name'],
            'secure_name' => $secureFileName,
            'path' => $destination,
            'size' => $file['size'],
            'type' => $file['type']
        ];
    }
    
    private function validateUpload(array $file): void
    {
        // Check for upload errors
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new Exception($this->getUploadError($file['error']));
        }
        
        // Check file size
        if ($file['size'] > $this->maxFileSize) {
            throw new Exception("File too large. Maximum size: " . $this->formatBytes($this->maxFileSize));
        }
        
        // Check if file is empty
        if ($file['size'] === 0) {
            throw new Exception('Empty file not allowed');
        }
        
        // Validate file name
        if (empty($file['name'])) {
            throw new Exception('Invalid file name');
        }
    }
    
    private function performSecurityChecks(array $file): void
    {
        // Check file extension
        $this->validateFileExtension($file['name']);
        
        // Check MIME type
        $this->validateMimeType($file['tmp_name']);
        
        // Check file signature (magic numbers)
        $this->validateFileSignature($file['tmp_name']);
        
        // Scan for malicious content
        $this->scanForMaliciousContent($file['tmp_name']);
        
        // Check for dangerous file patterns
        $this->checkDangerousPatterns($file['name']);
    }
    
    private function validateFileExtension(string $filename): void
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        
        if (empty($this->allowedExtensions)) {
            // Default denied extensions
            $deniedExtensions = [
                'php', 'php3', 'php4', 'php5', 'phtml', 'asp', 'aspx', 'jsp', 'jspx',
                'exe', 'bat', 'cmd', 'com', 'scr', 'vbs', 'js', 'jar', 'py', 'pl',
                'sh', 'cgi', 'htaccess', 'htpasswd'
            ];
            
            if (in_array($extension, $deniedExtensions)) {
                throw new Exception("File type '.{$extension}' is not allowed");
            }
        } else {
            if (!in_array($extension, $this->allowedExtensions)) {
                throw new Exception("File type '.{$extension}' is not allowed");
            }
        }
    }
    
    private function validateMimeType(string $filePath): void
    {
        if (empty($this->allowedTypes)) {
            return; // Skip if no specific types defined
        }
        
        // Get MIME type using multiple methods for accuracy
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $filePath);
        finfo_close($finfo);
        
        if (!in_array($mimeType, $this->allowedTypes)) {
            throw new Exception("MIME type '{$mimeType}' is not allowed");
        }
    }
    
    private function validateFileSignature(string $filePath): void
    {
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            throw new Exception('Cannot read file for signature validation');
        }
        
        $header = fread($handle, 20); // Read first 20 bytes
        fclose($handle);
        
        // Check for common executable signatures
        $dangerousSignatures = [
            'MZ',                          // PE executables
            "\x7FELF",                     // ELF executables
            "\xCA\xFE\xBA\xBE",          // Java class files
            "\xFE\xED\xFA\xCE",          // Mach-O executables
            '<?php',                       // PHP files
            '<script',                     // HTML/JS
            'PK',                          // ZIP files (could contain executables)
        ];
        
        foreach ($dangerousSignatures as $signature) {
            if (strpos($header, $signature) === 0) {
                throw new Exception('File contains dangerous signature');
            }
        }
    }
    
    private function scanForMaliciousContent(string $filePath): void
    {
        $content = file_get_contents($filePath);
        
        // Check for PHP code in non-PHP files
        $phpPatterns = [
            '/<\?php/i',
            '/<\?=/i',
            '/<script[^>]*language\s*=\s*["\']?php["\']?/i',
        ];
        
        foreach ($phpPatterns as $pattern) {
            if (preg_match($pattern, $content)) {
                throw new Exception('File contains potentially malicious PHP code');
            }
        }
        
        // Check for JavaScript in image files
        if ($this->isImageFile($filePath)) {
            $jsPatterns = [
                '/<script/i',
                '/javascript:/i',
                '/on\w+\s*=/i', // Event handlers like onclick, onload
            ];
            
            foreach ($jsPatterns as $pattern) {
                if (preg_match($pattern, $content)) {
                    throw new Exception('Image file contains malicious JavaScript');
                }
            }
        }
    }
    
    private function checkDangerousPatterns(string $filename): void
    {
        // Check for path traversal
        if (strpos($filename, '../') !== false || strpos($filename, '..\\') !== false) {
            throw new Exception('Path traversal detected in filename');
        }
        
        // Check for null bytes
        if (strpos($filename, "\0") !== false) {
            throw new Exception('Null byte detected in filename');
        }
        
        // Check for double extensions
        if (preg_match('/\.(php|asp|jsp|exe|bat)\./i', $filename)) {
            throw new Exception('Double extension detected');
        }
    }
    
    private function generateSecureFileName(string $originalName): string
    {
        $extension = pathinfo($originalName, PATHINFO_EXTENSION);
        $baseName = pathinfo($originalName, PATHINFO_FILENAME);
        
        // Sanitize base name
        $baseName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $baseName);
        $baseName = trim($baseName, '_-');
        
        if (empty($baseName)) {
            $baseName = 'file';
        }
        
        // Generate unique identifier
        $timestamp = time();
        $random = bin2hex(random_bytes(8));
        
        return "{$baseName}_{$timestamp}_{$random}.{$extension}";
    }
    
    private function normalizeFilesArray(array $files): array
    {
        $normalized = [];
        
        if (isset($files['name']) && is_array($files['name'])) {
            // Multiple files
            foreach ($files['name'] as $index => $name) {
                $normalized[] = [
                    'name' => $files['name'][$index],
                    'type' => $files['type'][$index],
                    'tmp_name' => $files['tmp_name'][$index],
                    'error' => $files['error'][$index],
                    'size' => $files['size'][$index]
                ];
            }
        } else {
            // Single file
            $normalized[] = $files;
        }
        
        return $normalized;
    }
    
    private function protectUploadDirectory(): void
    {
        $htaccessContent = "
# Deny execution of scripts
<Files \"*\">
    SetHandler default-handler
</Files>

# Deny access to PHP files
<FilesMatch \"\\.php$\">
    Order Deny,Allow
    Deny from all
</FilesMatch>

# Prevent directory browsing
Options -Indexes

# Additional security headers
<IfModule mod_headers.c>
    Header set X-Content-Type-Options nosniff
    Header set Content-Disposition attachment
</IfModule>
";
        
        file_put_contents($this->uploadDir . '/.htaccess', $htaccessContent);
    }
    
    private function isImageFile(string $filePath): bool
    {
        $imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $filePath);
        finfo_close($finfo);
        
        return in_array($mimeType, $imageTypes);
    }
    
    private function getUploadError(int $error): string
    {
        $errors = [
            UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive',
            UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive',
            UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
            UPLOAD_ERR_NO_FILE => 'No file was uploaded',
            UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
            UPLOAD_ERR_EXTENSION => 'File upload stopped by extension'
        ];
        
        return $errors[$error] ?? 'Unknown upload error';
    }
    
    private function formatBytes(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        
        return round($bytes, 2) . ' ' . $units[$i];
    }
}
?>

Image-Specific Security Measures

Image Upload Handler

<?php
class SecureImageUpload extends SecureFileUpload
{
    private $allowedImageTypes = [
        'image/jpeg',
        'image/png',
        'image/gif',
        'image/webp'
    ];
    
    private $maxWidth = 2048;
    private $maxHeight = 2048;
    
    public function __construct(string $uploadDir, int $maxFileSize = 2097152) // 2MB for images
    {
        parent::__construct($uploadDir, $maxFileSize);
        $this->setAllowedTypes($this->allowedImageTypes);
        $this->setAllowedExtensions(['jpg', 'jpeg', 'png', 'gif', 'webp']);
    }
    
    public function setMaxDimensions(int $width, int $height): self
    {
        $this->maxWidth = $width;
        $this->maxHeight = $height;
        return $this;
    }
    
    protected function performSecurityChecks(array $file): void
    {
        parent::performSecurityChecks($file);
        
        // Additional image-specific checks
        $this->validateImageDimensions($file['tmp_name']);
        $this->validateImageIntegrity($file['tmp_name']);
        $this->stripExifData($file['tmp_name']);
    }
    
    private function validateImageDimensions(string $filePath): void
    {
        $imageInfo = getimagesize($filePath);
        
        if ($imageInfo === false) {
            throw new Exception('Invalid image file');
        }
        
        [$width, $height] = $imageInfo;
        
        if ($width > $this->maxWidth || $height > $this->maxHeight) {
            throw new Exception("Image dimensions too large. Maximum: {$this->maxWidth}x{$this->maxHeight}");
        }
        
        // Check for extremely small images (possible exploits)
        if ($width < 1 || $height < 1) {
            throw new Exception('Invalid image dimensions');
        }
    }
    
    private function validateImageIntegrity(string $filePath): void
    {
        $imageInfo = getimagesize($filePath);
        
        if ($imageInfo === false) {
            throw new Exception('Corrupted or invalid image file');
        }
        
        // Try to create image resource to verify integrity
        $mimeType = $imageInfo['mime'];
        
        switch ($mimeType) {
            case 'image/jpeg':
                $image = imagecreatefromjpeg($filePath);
                break;
            case 'image/png':
                $image = imagecreatefrompng($filePath);
                break;
            case 'image/gif':
                $image = imagecreatefromgif($filePath);
                break;
            case 'image/webp':
                $image = imagecreatefromwebp($filePath);
                break;
            default:
                throw new Exception('Unsupported image type');
        }
        
        if ($image === false) {
            throw new Exception('Failed to process image - possibly corrupted');
        }
        
        imagedestroy($image);
    }
    
    private function stripExifData(string $filePath): void
    {
        // Strip EXIF data for privacy and security
        $imageInfo = getimagesize($filePath);
        $mimeType = $imageInfo['mime'];
        
        switch ($mimeType) {
            case 'image/jpeg':
                $image = imagecreatefromjpeg($filePath);
                if ($image) {
                    imagejpeg($image, $filePath, 90);
                    imagedestroy($image);
                }
                break;
            
            case 'image/png':
                $image = imagecreatefrompng($filePath);
                if ($image) {
                    imagepng($image, $filePath, 9);
                    imagedestroy($image);
                }
                break;
        }
    }
}
?>

File Upload Form Security

Secure Upload Form

<?php
class UploadForm
{
    public static function render(string $action, string $csrfToken): string
    {
        return '
        <form action="' . htmlspecialchars($action) . '" method="post" enctype="multipart/form-data">
            <input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">
            <input type="hidden" name="MAX_FILE_SIZE" value="5242880">
            
            <div class="upload-field">
                <label for="files">Choose Files:</label>
                <input type="file" 
                       name="files[]" 
                       id="files" 
                       multiple 
                       accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx"
                       required>
                <small>Max 5MB per file. Allowed: JPG, PNG, GIF, PDF, DOC, DOCX</small>
            </div>
            
            <div class="upload-progress" style="display: none;">
                <div class="progress-bar">
                    <div class="progress-fill"></div>
                </div>
                <span class="progress-text">0%</span>
            </div>
            
            <button type="submit">Upload Files</button>
        </form>
        
        <script>
        document.getElementById("files").addEventListener("change", function() {
            validateClientSide(this.files);
        });
        
        function validateClientSide(files) {
            const maxSize = 5 * 1024 * 1024; // 5MB
            const allowedTypes = ["image/jpeg", "image/png", "image/gif", "application/pdf"];
            
            for (let file of files) {
                if (file.size > maxSize) {
                    alert("File " + file.name + " is too large");
                    return false;
                }
                
                if (!allowedTypes.includes(file.type)) {
                    alert("File " + file.name + " has invalid type");
                    return false;
                }
            }
        }
        </script>';
    }
}
?>

Advanced Security Features

File Quarantine System

<?php
class FileQuarantine
{
    private $quarantineDir;
    private $approvedDir;
    
    public function __construct(string $quarantineDir, string $approvedDir)
    {
        $this->quarantineDir = rtrim($quarantineDir, '/');
        $this->approvedDir = rtrim($approvedDir, '/');
        
        // Create directories
        if (!is_dir($this->quarantineDir)) {
            mkdir($this->quarantineDir, 0700, true);
        }
        
        if (!is_dir($this->approvedDir)) {
            mkdir($this->approvedDir, 0755, true);
        }
    }
    
    public function quarantineFile(string $filePath, array $metadata): string
    {
        $quarantineId = uniqid('q_', true);
        $quarantinePath = $this->quarantineDir . '/' . $quarantineId;
        
        // Move file to quarantine
        if (!rename($filePath, $quarantinePath)) {
            throw new Exception('Failed to quarantine file');
        }
        
        // Save metadata
        $metadataPath = $quarantinePath . '.meta';
        file_put_contents($metadataPath, json_encode([
            'original_name' => $metadata['original_name'],
            'mime_type' => $metadata['mime_type'],
            'size' => $metadata['size'],
            'quarantined_at' => time(),
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ]));
        
        return $quarantineId;
    }
    
    public function approveFile(string $quarantineId, string $approvedName): bool
    {
        $quarantinePath = $this->quarantineDir . '/' . $quarantineId;
        $metadataPath = $quarantinePath . '.meta';
        $approvedPath = $this->approvedDir . '/' . $approvedName;
        
        if (!file_exists($quarantinePath) || !file_exists($metadataPath)) {
            return false;
        }
        
        // Perform final security scan
        $this->performFinalScan($quarantinePath);
        
        // Move to approved directory
        if (rename($quarantinePath, $approvedPath)) {
            unlink($metadataPath);
            return true;
        }
        
        return false;
    }
    
    public function deleteQuarantinedFile(string $quarantineId): bool
    {
        $quarantinePath = $this->quarantineDir . '/' . $quarantineId;
        $metadataPath = $quarantinePath . '.meta';
        
        $success = true;
        
        if (file_exists($quarantinePath)) {
            $success = unlink($quarantinePath);
        }
        
        if (file_exists($metadataPath)) {
            $success = unlink($metadataPath) && $success;
        }
        
        return $success;
    }
    
    private function performFinalScan(string $filePath): void
    {
        // Implement virus scanning, additional security checks
        // This could integrate with ClamAV or other security tools
        
        // Example: Check file size hasn't changed
        $metadata = json_decode(file_get_contents($filePath . '.meta'), true);
        if (filesize($filePath) !== $metadata['size']) {
            throw new Exception('File modified in quarantine');
        }
    }
}
?>

Virus Scanning Integration

<?php
class VirusScanner
{
    private $clamavSocket;
    
    public function __construct(string $clamavSocket = '/var/run/clamav/clamd.ctl')
    {
        $this->clamavSocket = $clamavSocket;
    }
    
    public function scanFile(string $filePath): bool
    {
        if (!file_exists($filePath)) {
            throw new Exception('File not found for scanning');
        }
        
        // Try ClamAV first
        if ($this->isClamAvailable()) {
            return $this->scanWithClamAV($filePath);
        }
        
        // Fallback to basic pattern matching
        return $this->basicVirusScan($filePath);
    }
    
    private function isClamAvailable(): bool
    {
        return file_exists($this->clamavSocket) || 
               exec('which clamscan') !== '';
    }
    
    private function scanWithClamAV(string $filePath): bool
    {
        $command = "clamscan --no-summary --infected " . escapeshellarg($filePath);
        $output = [];
        $returnCode = 0;
        
        exec($command, $output, $returnCode);
        
        // Return code 0 = clean, 1 = infected, 2 = error
        if ($returnCode === 1) {
            throw new Exception('Virus detected: ' . implode(' ', $output));
        } elseif ($returnCode === 2) {
            throw new Exception('Virus scanner error: ' . implode(' ', $output));
        }
        
        return true; // Clean
    }
    
    private function basicVirusScan(string $filePath): bool
    {
        $content = file_get_contents($filePath, false, null, 0, 1024 * 1024); // Read first 1MB
        
        // Basic malware signatures
        $malwareSignatures = [
            'EICAR-STANDARD-ANTIVIRUS-TEST-FILE',
            'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR',
            'eval(base64_decode(',
            'eval(gzinflate(',
            'eval(str_rot13(',
            'system(',
            'exec(',
            'shell_exec(',
            'passthru(',
            'file_get_contents("http',
            'curl_exec(',
            'fopen("http',
        ];
        
        foreach ($malwareSignatures as $signature) {
            if (stripos($content, $signature) !== false) {
                throw new Exception('Potential malware detected');
            }
        }
        
        return true;
    }
}
?>

Complete Upload Handler Example

<?php
class CompleteUploadHandler
{
    private $uploader;
    private $quarantine;
    private $virusScanner;
    private $database;
    
    public function __construct()
    {
        $this->uploader = new SecureFileUpload('/var/uploads/temp');
        $this->quarantine = new FileQuarantine('/var/uploads/quarantine', '/var/uploads/approved');
        $this->virusScanner = new VirusScanner();
        $this->database = new PDO(/* database config */);
        
        // Configure uploader
        $this->uploader
            ->setAllowedExtensions(['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'])
            ->setMaxFileSize(5242880) // 5MB
            ->setMaxFiles(5);
    }
    
    public function handleUpload(): array
    {
        try {
            // Validate CSRF token
            $this->validateCSRFToken();
            
            // Process uploads
            $results = $this->uploader->upload($_FILES['files']);
            
            $processedResults = [];
            
            foreach ($results as $result) {
                if ($result['success']) {
                    $processedResult = $this->processUploadedFile($result);
                    $processedResults[] = $processedResult;
                } else {
                    $processedResults[] = $result;
                }
            }
            
            return $processedResults;
            
        } catch (Exception $e) {
            error_log("Upload error: " . $e->getMessage());
            return [['success' => false, 'error' => 'Upload failed']];
        }
    }
    
    private function processUploadedFile(array $fileResult): array
    {
        try {
            // Virus scan
            $this->virusScanner->scanFile($fileResult['path']);
            
            // Quarantine file for manual review if needed
            $quarantineId = $this->quarantine->quarantineFile(
                $fileResult['path'],
                [
                    'original_name' => $fileResult['original_name'],
                    'mime_type' => $fileResult['type'],
                    'size' => $fileResult['size']
                ]
            );
            
            // Log upload
            $this->logUpload($fileResult, $quarantineId);
            
            return [
                'success' => true,
                'quarantine_id' => $quarantineId,
                'original_name' => $fileResult['original_name'],
                'message' => 'File uploaded and queued for approval'
            ];
            
        } catch (Exception $e) {
            // Clean up file
            if (file_exists($fileResult['path'])) {
                unlink($fileResult['path']);
            }
            
            return [
                'success' => false,
                'error' => $e->getMessage(),
                'original_name' => $fileResult['original_name']
            ];
        }
    }
    
    private function validateCSRFToken(): void
    {
        if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
            throw new Exception('Invalid CSRF token');
        }
    }
    
    private function logUpload(array $fileResult, string $quarantineId): void
    {
        $stmt = $this->database->prepare("
            INSERT INTO upload_logs (
                quarantine_id, original_name, file_size, mime_type, 
                ip_address, user_agent, uploaded_at
            ) VALUES (?, ?, ?, ?, ?, ?, NOW())
        ");
        
        $stmt->execute([
            $quarantineId,
            $fileResult['original_name'],
            $fileResult['size'],
            $fileResult['type'],
            $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ]);
    }
}
?>

Usage Example

<?php
session_start();

// Generate CSRF token
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $handler = new CompleteUploadHandler();
    $results = $handler->handleUpload();
    
    header('Content-Type: application/json');
    echo json_encode($results);
    exit;
}

// Display form
echo UploadForm::render('/upload.php', $_SESSION['csrf_token']);
?>

Best Practices Summary

  1. Never trust user input: Validate everything on the server side
  2. Use whitelist validation: Only allow known safe file types
  3. Store uploads outside web root: Prevent direct execution
  4. Scan for malware: Use virus scanning tools
  5. Implement file quarantine: Review files before making them accessible
  6. Generate secure filenames: Prevent path traversal and conflicts
  7. Set proper permissions: Limit file system access
  8. Log all uploads: Maintain audit trail
  9. Use CSRF protection: Prevent unauthorized uploads
  10. Implement rate limiting: Prevent abuse and DoS attacks

Secure file uploads require multiple layers of protection. Always assume that attackers will try to exploit your upload functionality and implement comprehensive security measures to protect your application and users.