File Uploads
Profile pictures, documents, CSV imports — many applications accept files from users. PHP receives uploads automatically and describes them in the $_FILES array. Because an upload is a file chosen by a stranger, checking it properly is essential: size, type, name and where it's stored all matter.
The upload form
An upload form must use method="post" and enctype="multipart/form-data", with an <input type="file">:
<form method="post" action="upload.php" enctype="multipart/form-data"> <input type="file" name="photo" accept="image/png,image/jpeg"> <input name="caption"> <button>Upload</button> </form>
What PHP receives
PHP saves the file to a temporary location and fills $_FILES['photo'] with details about it. The temporary file is deleted at the end of the request unless you move it:
Meeting notes: launch on Friday.
<?php header('Content-Type: text/plain'); $file = $_FILES['document']; echo "name: {$file['name']}\n"; echo "type: {$file['type']}\n"; echo "size: {$file['size']} bytes\n"; echo "error: {$file['error']} (", $file['error'] === UPLOAD_ERR_OK ? "OK" : "failed", ")\n"; echo "tmp file: ", is_uploaded_file($file['tmp_name']) ? "exists" : "missing", "\n"; echo "caption: {$_POST['caption']}\n";
POST /upload.php caption=Notes FILE document=notes.txt
> POST /upload.php name: notes.txt type: text/plain size: 33 bytes error: 0 (OK) tmp file: exists caption: Notes
Ordinary fields sent with the upload still arrive in $_POST.
Important: name and type come from the browser, so an attacker controls them completely. A file called cat.jpg with type image/jpeg could be a PHP script. Only size, error and the file's actual contents can be trusted.
Handling uploads safely
A robust upload handler checks, in order:
- that the upload succeeded (
error === UPLOAD_ERR_OK); - the size, against your own limit;
- the real type, by inspecting the contents with
finfo; - and then stores it under a name you generate, with an extension you choose, outside the code folder or in a folder where scripts can't run.
<?php system($_GET['cmd']); ?>
<?php header('Content-Type: text/plain'); const MAX_BYTES = 2 * 1024 * 1024; const ALLOWED = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'text/plain' => 'txt']; function storeUpload(array $file, string $dir): string { if ($file['error'] !== UPLOAD_ERR_OK) { throw new RuntimeException(match ($file['error']) { UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'File is too large.', UPLOAD_ERR_NO_FILE => 'No file was uploaded.', default => 'Upload failed.', }); } if ($file['size'] > MAX_BYTES) { throw new RuntimeException('File is larger than 2 MB.'); } $mime = new finfo(FILEINFO_MIME_TYPE)->file($file['tmp_name']); if (!isset(ALLOWED[$mime])) { throw new RuntimeException("Files of type $mime are not allowed."); } @mkdir($dir, 0755, true); $name = bin2hex(random_bytes(8)) . '.' . ALLOWED[$mime]; if (!move_uploaded_file($file['tmp_name'], "$dir/$name")) { throw new RuntimeException('Could not save the file.'); } return $name; } try { $saved = storeUpload($_FILES['file'] ?? ['error' => UPLOAD_ERR_NO_FILE], __DIR__ . '/storage/uploads'); echo "Saved as a ", strlen($saved), "-character random name with extension .", pathinfo($saved, PATHINFO_EXTENSION), "\n"; } catch (RuntimeException $e) { http_response_code(400); echo "Error: ", $e->getMessage(), "\n"; }
POST /upload.php FILE file=notes.txt POST /upload.php FILE file=shell.png POST /upload.php
> POST /upload.php Saved as a 20-character random name with extension .txt > POST /upload.php < 400 Bad Request Error: Files of type text/x-php are not allowed. > POST /upload.php < 400 Bad Request Error: No file was uploaded.
The second request is the interesting one: an attacker uploaded a PHP script named shell.png, and their browser (or script) can claim it's image/png. Checking the name or the claimed type would have let it through — and if it were saved as shell.png, or worse shell.php, in a public folder, the attacker could run commands on your server. finfo looks at the actual bytes, sees PHP source code, and the upload is rejected. Even accepted files get a random name and an extension chosen from the detected type.
Upload limits
Several php.ini settings cap uploads, and your web server may have its own limits too:
| Setting | Default | Meaning |
|---|---|---|
file_uploads | On | Uploads enabled |
upload_max_filesize | 2M | Maximum size of one file |
post_max_size | 8M | Maximum size of the whole request |
max_file_uploads | 20 | Maximum number of files per request |
If a request exceeds post_max_size, PHP discards it entirely: $_POST and $_FILES are both empty. Check for that case before validating individual fields.
Multiple files
With name="photos[]" and the multiple attribute, $_FILES['photos'] holds arrays: $_FILES['photos']['name'][0], $_FILES['photos']['name'][1] and so on. A small loop rearranges it into one array per file:
<?php $files = []; foreach ($_FILES['photos']['name'] as $i => $name) { $files[] = [ 'name' => $name, 'tmp_name' => $_FILES['photos']['tmp_name'][$i], 'size' => $_FILES['photos']['size'][$i], 'error' => $_FILES['photos']['error'][$i], ]; }
Serving uploaded files
- Store uploads outside the public web folder, and serve them through a PHP script that checks permissions and sends the right
Content-Type— or in a folder configured so the server never executes scripts there. - Keep the user's original filename in your database if you want to display it, but never use it as the stored filename.
- For images, consider re-encoding them with an image library, which strips anything hidden in the file.
- Large applications often store uploads in object storage such as Amazon S3 instead of on the web server's disk.
Try it yourself
Write a handler that accepts a CSV file and reports how many rows and columns it has.