How to Secure File Uploads in PHP

Allowing users to upload files is a common requirement for modern web applications, but it also introduces significant security risks. This article outlines the essential security checks you must perform when handling file uploads in PHP to protect your server from malicious attacks, unauthorized execution, and system compromise.

1. Implement a Strict Extension Whitelist

Never use a blacklist to block dangerous extensions like .php or .exe, as attackers can often bypass these using alternative extensions (like .phtml, .php5, or .phar). Instead, define a strict whitelist of allowed extensions (e.g., ['jpg', 'jpeg', 'png', 'pdf']) and check the uploaded file extension against this list.

$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf'];
$fileExtension = strtolower(pathinfo($_FILES['uploaded_file']['name'], PATHINFO_EXTENSION));

if (!in_array($fileExtension, $allowedExtensions)) {
    die("File type not allowed.");
}

2. Verify the Real MIME Type

Do not rely on the MIME type sent by the browser ($_FILES['uploaded_file']['type']), as this can easily be spoofed by an attacker. Use PHP’s finfo extension to analyze the actual content of the file and verify its MIME type.

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['uploaded_file']['tmp_name']);

$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mimeType, $allowedMimeTypes)) {
    die("Invalid file content.");
}

3. Rename Uploaded Files

Keep attackers from predicting the file path or overwriting existing system files by renaming every uploaded file. Generate a cryptographically secure random name using random_bytes() or a UUID, and append the validated extension.

$newFileName = bin2hex(random_bytes(16)) . '.' . $fileExtension;

4. Limit the Maximum File Size

Restrict the size of uploaded files to prevent Denial of Service (DoS) attacks that attempt to fill your server’s disk space. Set limits both in your php.ini configuration and within your PHP application script.

In php.ini:

upload_max_filesize = 5M
post_max_size = 5M

In PHP:

$maxFileSize = 5 * 1024 * 1024; // 5 MB
if ($_FILES['uploaded_file']['size'] > $maxFileSize) {
    die("File is too large.");
}

5. Store Files Outside the Web Root

The most effective way to prevent users from executing malicious scripts they upload is to store the files in a directory that is not accessible via a public URL (outside the public_html or www directory). If users need to download these files, use a PHP script to read the file content and stream it to the browser.

6. Disable Directory Execution

If you must store uploaded files within the web root, configure your web server to prevent the execution of scripts in the upload directory.

For Apache, place a .htaccess file inside the upload folder with the following directive:

Header set Content-Security-Policy "default-src 'self';"
<FilesMatch "\.(php|pl|py|jsp|asp|sh|cgi)$">
    ForceType text/plain
    Deny from all
</FilesMatch>

For Nginx, add a rule to your server configuration blocking PHP execution inside the uploads folder:

location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

7. Prevent Directory Traversal

Attackers may attempt to manipulate the filename to save files in sensitive system directories (e.g., using ../../etc/passwd). Always use basename() on the file path to strip directory traversal sequences before handling the file.

$safeFilename = basename($_FILES['uploaded_file']['name']);