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!";
}