PHP File Upload — Secure File Handling, Validation, and Storage
In this tutorial, you will learn about PHP File Upload. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP file upload allows users to send files from their browser to the server through HTML forms, where $_FILES captures the file metadata and move_uploaded_file stores it securely with proper validation.
Why It Matters
File upload is one of the most common web features and one of the most dangerous. A poorly implemented upload handler allows attackers to upload executable PHP files, malware, or large files that fill your disk. Understanding secure file upload -- validating type, size, and content -- is essential for any application that accepts user files.
Real-World Use
Social media platforms upload profile pictures and media content. Cloud storage services like Dropbox handle multi-gigabyte file uploads. Job sites accept PDF resumes. E-commerce sites upload product images. WordPress media library uploads images, PDFs, and videos. DodaTech's support portal allows users to upload diagnostic logs for analysis.
What You Will Learn
- HTML form setup for file uploads
- The $_FILES superglobal structure
- File validation: type, size, extension, content
- Secure file storage with random filenames
- Multiple file upload handling
- Error codes and troubleshooting
- Upload progress tracking
Learning Path
flowchart LR A[Headers] --> B[File Upload
You are here] B --> C[JSON API] C --> D[Error Handling] D --> E[Classes and OOP] style B fill:#f90,color:#fff
HTML Form for File Uploads
File upload forms require specific attributes:
<form method="post" action="upload.php" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="5242880">
<input type="file" name="document" accept=".pdf,.jpg,.png">
<button type="submit">Upload</button>
</form>
The enctype="multipart/form-data" is required. Without it, files are not sent. The optional MAX_FILE_SIZE hidden field hints the browser to limit file size, but you must still validate on the server.
The $_FILES Structure
When a file is uploaded, $_FILES contains an array with this structure:
$_FILES['document'] = [
'name' => 'report.pdf', // Original filename
'type' => 'application/pdf', // MIME type (from client)
'tmp_name' => '/tmp/phpXXXXXX', // Temporary path
'error' => UPLOAD_ERR_OK, // Error code
'size' => 1024000, // Size in bytes
];
Basic Upload Handler
A secure upload handler with validation:
<?php
$uploadDir = __DIR__ . '/uploads/';
$maxFileSize = 5 * 1024 * 1024; // 5MB
$allowedExtensions = ['pdf', 'jpg', 'png', 'gif'];
$allowedMimeTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('Invalid request method');
}
if (!isset($_FILES['document'])) {
die('No file uploaded');
}
$file = $_FILES['document'];
$errors = [];
// Check upload error
if ($file['error'] !== UPLOAD_ERR_OK) {
$errorMessages = [
UPLOAD_ERR_INI_SIZE => 'File exceeds server size limit',
UPLOAD_ERR_FORM_SIZE => 'File exceeds form size limit',
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',
];
die($errorMessages[$file['error']] ?? 'Unknown upload error');
}
// Validate file size
if ($file['size'] > $maxFileSize) {
die('File too large. Maximum is 5MB.');
}
// Validate extension
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions)) {
die('Invalid file extension');
}
// Validate MIME type (by file content, not client-reported)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedMimeTypes)) {
die('Invalid file type');
}
// Generate a secure random filename
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDir . $newFilename;
// Ensure upload directory exists
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Move the file
if (move_uploaded_file($file['tmp_name'], $destination)) {
echo "File uploaded successfully: $newFilename\n";
echo "Original name: " . htmlspecialchars($file['name']) . "\n";
echo "Size: " . number_format($file['size']) . " bytes\n";
echo "Type: " . htmlspecialchars($mimeType) . "\n";
} else {
echo "Failed to move uploaded file.";
}
Multiple File Uploads
Handle multiple files with array notation in the form:
<form method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple accept="image/*">
<button type="submit">Upload Images</button>
</form>
PHP processing:
<?php
$uploadDir = __DIR__ . '/uploads/';
$uploaded = [];
$errors = [];
foreach ($_FILES['files']['name'] as $index => $name) {
$file = [
'name' => $_FILES['files']['name'][$index],
'type' => $_FILES['files']['type'][$index],
'tmp_name' => $_FILES['files']['tmp_name'][$index],
'error' => $_FILES['files']['error'][$index],
'size' => $_FILES['files']['size'][$index],
];
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "File '$name' failed: error code {$file['error']}";
continue;
}
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$newName = bin2hex(random_bytes(8)) . '.' . $ext;
$dest = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$uploaded[] = $newName;
} else {
$errors[] = "Failed to save '$name'";
}
}
echo "Uploaded: " . implode(', ', $uploaded) . "\n";
if ($errors) {
echo "Errors: " . implode(', ', $errors) . "\n";
}
File Upload Security Checklist
1. Validate File Extension
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed = ['jpg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allowed)) {
die('Extension not allowed');
}
2. Validate MIME Type by Content
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
Do not trust $_FILES['file']['type'] -- it is sent by the client and can be faked.
3. Validate File Size
$maxSize = 10 * 1024 * 1024; // 10MB
if ($file['size'] > $maxSize || $file['size'] === 0) {
die('Invalid file size');
}
4. Use Random Filenames
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
Never use user-supplied filenames. They can contain path traversal characters like ../.
5. Store Outside Web Root When Possible
Store uploads outside the document root and serve them through a PHP script that adds security checks.
6. Disable Execution in Upload Directory
Add an .htaccess file in your uploads directory:
<FilesMatch "\.(php|pl|py|jsp|asp|sh|cgi)$">
Order Deny,Allow
Deny from all
</FilesMatch>
Or for Nginx:
location /uploads/ {
location ~ \.php$ { deny all; }
}
Upload Error Codes
| Constant | Value | Meaning |
|---|---|---|
| UPLOAD_ERR_OK | 0 | Success |
| UPLOAD_ERR_INI_SIZE | 1 | Exceeds upload_max_filesize in php.ini |
| UPLOAD_ERR_FORM_SIZE | 2 | Exceeds MAX_FILE_SIZE in form |
| UPLOAD_ERR_PARTIAL | 3 | Only partially uploaded |
| UPLOAD_ERR_NO_FILE | 4 | No file was uploaded |
| UPLOAD_ERR_NO_TMP_DIR | 6 | Missing temporary folder |
| UPLOAD_ERR_CANT_WRITE | 7 | Failed to write to disk |
PHP Configuration for Uploads
Set appropriate limits in php.ini:
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 20
upload_tmp_dir = /tmp
Common Mistakes
1. Not Checking upload_max_filesize
If a file exceeds upload_max_filesize, $_FILES is still set but error is UPLOAD_ERR_INI_SIZE. Always check the error code.
2. Trusting the Client-Reported MIME Type
$mime = $_FILES['file']['type']; // Can be faked!
Use finfo to detect the actual file type from content.
3. Using User Filenames Without Sanitization
move_uploaded_file($tmp, 'uploads/' . $_FILES['file']['name']);
// Path traversal: name could be "../../etc/passwd"
Always generate a new random filename.
4. Not Limiting File Size
Large files can fill your disk and cause denial of service. Always enforce both upload_max_filesize and application-level limits.
5. Storing Uploads in the Web Root
Uploads in the web root are directly accessible. Users could upload HTML files with JavaScript and perform XSS. Store uploads outside web root or restrict access.
Practice Questions
Why is enctype="multipart/form-data" required for file uploads? It tells the browser to encode the form data as multipart MIME, which is required for file binary data.
How do you determine the actual file type of an upload? Use
finfo_file()withFILEINFO_MIME_TYPEto examine the file content, not the client-reported type.Why should you use random filenames for uploaded files? To prevent path traversal attacks and filename collisions. Random names prevent users from guessing or accessing each other's files.
What does move_uploaded_file do that rename does not? move_uploaded_file performs additional security checks to ensure the file was actually uploaded via PHP, preventing local file inclusion attacks.
Challenge: Build a complete file gallery that allows image uploads, validates type and size, generates thumbnails, and displays uploaded images.
Mini Project: Image Upload with Validation
Build a secure image uploader:
<?php
define('UPLOAD_DIR', __DIR__ . '/gallery/');
define('MAX_SIZE', 5 * 1024 * 1024);
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
define('ALLOWED_EXTS', ['jpg', 'jpeg', 'png', 'webp']);
function uploadImage(array $file): array {
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload error code: ' . $file['error']];
}
if ($file['size'] > MAX_SIZE || $file['size'] === 0) {
return ['success' => false, 'error' => 'Invalid file size'];
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTS)) {
return ['success' => false, 'error' => 'Extension not allowed'];
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ALLOWED_TYPES)) {
return ['success' => false, 'error' => 'Invalid image type'];
}
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
$newName = bin2hex(random_bytes(12)) . '.' . $ext;
$dest = UPLOAD_DIR . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
return ['success' => false, 'error' => 'Failed to save file'];
}
return ['success' => true, 'filename' => $newName];
}
FAQ
What is Next
Now that you can handle file uploads, proceed to JSON API to build RESTful API endpoints that return structured data. Then read Error Handling to learn robust error management.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro