PHP 8.5 file upload tutorial
Uploading a file with HTML and PHP is simple, but accepting arbitrary files from a browser is also a security-sensitive operation.
A modern implementation should do more than call move_uploaded_file(). It should validate the upload result, enforce a size limit, inspect the MIME type, avoid trusting the client-supplied filename and write the file only to a controlled destination.
This example targets PHP 8.5 and a normal Apache/PHP deployment.
HTML5 file upload form
The browser form must use method="post" and enctype="multipart/form-data".
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PHP File Upload Example</title>
</head>
<body>
<h1>Upload a file</h1>
<form action="upload.php"
method="post"
enctype="multipart/form-data">
<label for="file">Choose a file:</label>
<input
id="file"
name="file"
type="file"
required
>
<button type="submit">
Upload
</button>
</form>
</body>
</html>The original example used type="button", which does not submit the form by itself. The corrected example uses a real submit button.
PHP 8.5 upload handler
Save the following as upload.php.
<?php
declare(strict_types=1);
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
$uploadDirectory = __DIR__ . '/uploads';
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Only POST requests are allowed.');
}
if (!isset($_FILES['file'])) {
http_response_code(400);
exit('No upload was received.');
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Upload failed with error code: ' . $file['error']);
}
if ($file['size'] > MAX_UPLOAD_BYTES) {
http_response_code(413);
exit('The uploaded file is too large.');
}
if (!is_dir($uploadDirectory)) {
if (!mkdir($uploadDirectory, 0750, true)
&& !is_dir($uploadDirectory)) {
http_response_code(500);
exit('Unable to create the upload directory.');
}
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!isset($allowedMimeTypes[$mimeType])) {
http_response_code(415);
exit('Unsupported file type.');
}
$extension = $allowedMimeTypes[$mimeType];
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDirectory . DIRECTORY_SEPARATOR . $filename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
http_response_code(500);
exit('The server could not save the uploaded file.');
}
header('Content-Type: text/plain; charset=utf-8');
echo "Upload successful: {$filename}";Why not trust the original filename?
The browser sends a client-side filename in $_FILES['file']['name'], but applications should not treat that value as a safe server filename.
A safer pattern is to generate a random server-side name and derive the extension from a MIME type that the server inspected itself.
$filename =
bin2hex(random_bytes(16))
. '.'
. $extension;This avoids collisions and reduces the risk of path manipulation or executable filenames being placed on the server.
Validate MIME type with finfo
Do not trust a file extension or a browser-provided content type. PHP's finfo API can inspect the uploaded temporary file:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file(
$file['tmp_name']
);The application can then compare the detected MIME type against a small allowlist.
Check PHP upload error codes
Always inspect $_FILES['file']['error']. A successful upload is represented by UPLOAD_ERR_OK.
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit(
'Upload failed with error code: '
. $file['error']
);
}This catches problems such as upload-size limits, partial uploads and temporary-directory failures before the application tries to move the file.
PHP upload size settings
Application-level limits are useful, but PHP also enforces configuration-level limits.
Important php.ini settings include:
file_uploads = On
upload_max_filesize = 5M
post_max_size = 6Mpost_max_size should be large enough to contain the uploaded file plus the rest of the multipart request.
Where should uploaded files be stored?
If users do not need direct browser access to the uploaded files, the safest location is usually outside Apache's public document root.
For example:
/var/www/example/public/ Apache document root
/var/www/example/uploads/ Private upload storageIf uploads must be publicly downloadable, consider serving them through application logic or a dedicated static location with execution disabled rather than dropping arbitrary files into the same directory as PHP scripts.
Apache directory permissions
The old recommendation to simply run chmod 775 on the upload directory is too broad to be useful everywhere.
The important requirement is that the account running PHP has write permission to the destination directory while unrelated users do not.
On a typical Linux deployment, ownership and permissions might be configured along these lines:
sudo mkdir -p /var/www/example/uploads
sudo chown www-data:www-data /var/www/example/uploads
sudo chmod 0750 /var/www/example/uploadsThe actual Apache/PHP account varies by distribution and server configuration, so verify it on the target system rather than copying ownership values blindly.
Test the upload
Open the HTML form in the browser, select an allowed file and submit it.
You can also test the endpoint with curl:
curl \
-F "file=@example.pdf" \
https://localhost/upload.phpA successful response looks similar to:
Upload successful: 0f5b8a4d6b9f....pdfPHP upload security checklist
- Accept uploads only through POST requests.
- Check
UPLOAD_ERR_OK. - Enforce an application-level size limit.
- Configure
upload_max_filesizeandpost_max_size. - Inspect the file with
finfo. - Use an allowlist of permitted MIME types.
- Generate the server-side filename yourself.
- Keep private uploads outside the public document root.
- Give the PHP process only the permissions it needs.
- Never allow an uploaded filename to determine an executable server path.
PHP's upload API is easy to use, but the secure version is more than a two-line call to $_FILES and move_uploaded_file(). Validate first, generate a safe destination name and store the file in a directory with deliberately configured permissions.