Secure File Uploads in PHP
Introduction
Secure file uploads in PHP are crucial for protecting web applications from dangerous threats such as malware uploads, remote code execution, and server-side attacks. Improperly handled file uploads can be exploited to take full control of your server.
In this comprehensive guide, we will cover how to:
- Handle file uploads in PHP safely
- Validate file types and sizes
- Prevent malicious uploads (like .php, .exe, .js)
- Implement secure upload directories
- Use MIME type verification
- Prevent filename spoofing
- Avoid overwriting existing files
Whether you're building a PHP image upload script, a document upload system, or a file-sharing application, these tips will help you stay secure.
1. File Upload Basics in PHP
PHP allows uploading files using a simple HTML form and the $_FILES superglobal.
Basic HTML Form
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="userfile">
<input type="submit" value="Upload">
</form>
PHP Upload Script
if ($_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
$tmpName = $_FILES['userfile']['tmp_name'];
$name = $_FILES['userfile']['name'];
move_uploaded_file($tmpName, "uploads/$name");
}
WARNING: This code is insecure! We'll now explore how to secure it properly.
2. Secure Upload Directory
Never store uploads in your web root (/public_html, /www). Instead, use a non-public folder outside of it:
/home/user/uploads/
Then access files through a script using a secure handler or token system.
This prevents direct access to uploaded files.
3. Validate File Type and Extension
Only allow safe file extensions like .jpg, .png, .pdf, and .docx. Avoid executable formats.
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'];
$extension = strtolower(pathinfo($_FILES['userfile']['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions)) {
die("Invalid file type.");
}
This blocks .php, .exe, .sh, and other malicious files.
4. Validate MIME Type with finfo
Relying on file extensions is not enough. Check the MIME type using finfo.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['userfile']['tmp_name']);
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowedMimeTypes)) {
die("Invalid file format.");
}
Prevents spoofed files that disguise .php as .jpg.
5. Sanitize the File Name
Sanitize the uploaded file name to prevent directory traversal and special character injection.
function sanitizeFileName($filename) {
$filename = basename($filename);
return preg_replace("/[^A-Za-z0-9_\-\.]/", '_', $filename);
}
$cleanName = sanitizeFileName($_FILES['userfile']['name']);
Stops attackers from using ../../../etc/passwd or <script>.jpg.
6. Rename Files Before Saving
Avoid saving files with user-provided names. Generate a unique, secure name:
$uniqueName = uniqid() . '.' . $extension;
move_uploaded_file($_FILES['userfile']['tmp_name'], "uploads/$uniqueName");
Prevents overwriting files and makes guessing harder.
7. Set Max File Size Limit
Prevent large file uploads that can consume server storage or lead to DoS attacks.
In HTML:
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB -->
In PHP:
$maxSize = 2 * 1024 * 1024; // 2MB
if ($_FILES['userfile']['size'] > $maxSize) {
die("File too large.");
}
Also set in php.ini:
upload_max_filesize = 2M
post_max_size = 8M
8. Use move_uploaded_file() Only
Never use copy() or rename() for moving uploaded files. Always use:
move_uploaded_file($_FILES['userfile']['tmp_name'], $destination);
This ensures the source is a valid uploaded file.
9. Prevent Script Execution
If you must store files in a web-accessible folder, disable PHP execution in it.
.htaccess (for Apache):
<FilesMatch "\.(php|php5|phtml)$">
Deny from all
</FilesMatch>
Options -ExecCGI
RemoveHandler .php .phtml .php3
Prevents execution of uploaded PHP backdoors.
10. Set Proper File Permissions
Use restrictive permissions when saving files:
chmod("uploads/$uniqueName", 0644);
Avoids execution or unintended write access.
11. Store Files Outside Public Directory
As a best practice, store uploaded files outside of your public folder and serve them using a PHP script:
// download.php?id=123
$filePath = "/var/uploads/{$fileId}.pdf";
if (file_exists($filePath)) {
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="download.pdf"');
readfile($filePath);
exit;
}
Adds an extra layer of access control.
12. Use Antivirus/Malware Scanning (Optional)
Integrate tools like ClamAV or VirusTotal API to scan uploaded files.
clamscan /path/to/uploaded/file
Useful for enterprise apps handling unknown user files.
Example: Full Secure File Upload Script
$allowedExtensions = ['jpg', 'png', 'pdf'];
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$maxSize = 2 * 1024 * 1024;
if ($_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
$tmp = $_FILES['userfile']['tmp_name'];
$originalName = $_FILES['userfile']['name'];
$size = $_FILES['userfile']['size'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmp);
if (!in_array($ext, $allowedExtensions) || !in_array($mime, $allowedMimeTypes)) {
die("Invalid file type.");
}
if ($size > $maxSize) {
die("File too large.");
}
$cleanName = sanitizeFileName($originalName);
$uniqueName = uniqid() . '.' . $ext;
move_uploaded_file($tmp, "uploads/$uniqueName");
chmod("uploads/$uniqueName", 0644);
echo "File uploaded successfully!";
}