$_FILES
What Is $_FILES?
In simple terms: $_FILES contains information about files a user uploads through a form — the original file name, its size, where PHP temporarily stored it, and whether the upload succeeded. Technically: $_FILES is a superglobal populated when a POST request is sent with Content-Type: multipart/form-data. For each file input, PHP saves the uploaded file to a temporary location and describes it in $_FILES. Unless your script moves the file somewhere permanent, PHP deletes it automatically when the request ends.
Why Uploads Need Special Care
File uploads are among the most security-sensitive features of a web application. An attacker who can upload a PHP script into a public folder and then request it can run any code on your server. Every upload must therefore be validated and stored safely — this lesson covers both.
The Upload Form
<form method="post" action="/upload.php" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/png, image/jpeg">
<button type="submit">Upload</button>
</form>
Three things are required:
method="post"— files cannot be sent with GET.enctype="multipart/form-data"— without it, the browser sends only the file name, and$_FILESstays empty.- A
nameattribute on the file input — it becomes the key in$_FILES. Theacceptattribute only filters the browser's file picker; it provides no security, because requests can be sent without the browser.
The Structure of $_FILES
After a user uploads profile photo.jpg, $_FILES looks like this:
[
'avatar' => [
'name' => 'profile photo.jpg', // original name from the client
'full_path' => 'profile photo.jpg', // path within an uploaded folder (PHP 8.1+)
'type' => 'image/jpeg', // MIME type claimed by the client
'tmp_name' => '/tmp/phpA1b2C3', // temporary file on the server
'error' => 0, // UPLOAD_ERR_OK
'size' => 52341, // size in bytes
],
]
| Key | Meaning | Trustworthy? |
|---|---|---|
name | Original file name | No — chosen by the client |
full_path | Relative path when a directory is uploaded (PHP 8.1+) | No |
type | MIME type sent by the browser | No — easily faked |
tmp_name | Path of the temporary file on the server | Yes (set by PHP) |
error | Upload status code | Yes |
size | Size in bytes | Yes (measured by PHP) |
Only tmp_name, error, and size are determined by PHP. Never use name or type for security decisions.
Upload Error Codes
Always check error first:
| Constant | Value | Meaning |
|---|---|---|
UPLOAD_ERR_OK | 0 | Upload succeeded |
UPLOAD_ERR_INI_SIZE | 1 | File exceeds upload_max_filesize in php.ini |
UPLOAD_ERR_FORM_SIZE | 2 | File exceeds the form's MAX_FILE_SIZE field |
UPLOAD_ERR_PARTIAL | 3 | File was only partially uploaded |
UPLOAD_ERR_NO_FILE | 4 | No file was selected |
UPLOAD_ERR_NO_TMP_DIR | 6 | Server has no temporary folder |
UPLOAD_ERR_CANT_WRITE | 7 | Server failed to write the file |
UPLOAD_ERR_EXTENSION | 8 | A PHP extension stopped the upload |
Turning Codes Into Messages
<?php
function uploadErrorMessage(int $code): ?string
{
return match ($code) {
UPLOAD_ERR_OK => null,
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The file is too large.',
UPLOAD_ERR_PARTIAL => 'The upload was interrupted. Please try again.',
UPLOAD_ERR_NO_FILE => 'Please choose a file.',
default => 'The server could not save the file. Please try later.',
};
}
foreach ([0, 1, 4, 7] as $code) {
var_dump(uploadErrorMessage($code));
}
Output
NULL
string(22) "The file is too large."
string(21) "Please choose a file."
string(53) "The server could not save the file. Please try later."
Server-side problems (codes 6, 7, 8) should also be logged, because they indicate a configuration issue rather than a user mistake.
Validating and Saving an Upload
Step by Step
- Check the error code.
- Check the size against your own limit.
- Detect the real file type from the file contents with
finfo— not fromtypeor the extension. - Generate a new file name on the server; never reuse the client's name.
- Move the file with
move_uploaded_file(), which also verifies that the file really came from an upload.
Example: Avatar Upload
<?php
// upload.php
function saveAvatar(array $file, string $targetDir): string
{
if (!isset($file['error']) || is_array($file['error'])) {
throw new RuntimeException('Invalid upload.');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException(uploadErrorMessage($file['error']));
}
$maxBytes = 2 * 1024 * 1024; // 2 MB
if ($file['size'] > $maxBytes) {
throw new RuntimeException('The file must be 2 MB or smaller.');
}
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
if (!isset($allowed[$mime])) {
throw new RuntimeException('Only JPEG, PNG, and WebP images are allowed.');
}
$fileName = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$destination = rtrim($targetDir, '/') . '/' . $fileName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('The server could not save the file.');
}
return $fileName;
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
try {
$saved = saveAvatar($_FILES['avatar'] ?? [], __DIR__ . '/../storage/avatars');
echo 'Uploaded as ' . htmlspecialchars($saved);
} catch (RuntimeException $e) {
http_response_code(422);
echo htmlspecialchars($e->getMessage());
}
}
Output After Uploading a Valid JPEG (the name is random each time)
Uploaded as 3f9a1c0e8b7d4a2e9c6b1f0a5d8e7c42.jpg
Why Each Step Matters
| Step | Protects Against |
|---|---|
is_array($file['error']) check | Requests that send several files where one was expected (avatar[]) |
| Error code check | Processing incomplete or missing files |
| Size limit | Disk exhaustion and oversized images |
finfo MIME detection | Files with a fake extension or fake type, such as shell.php renamed to photo.jpg |
| Extension from your allow-list | Saving a file with an executable extension like .php |
| Random file name | Overwriting other files, path traversal (../../index.php), and guessing file names |
move_uploaded_file() | Moving files that did not come from an upload |
Storage outside the web root (storage/) | Direct execution of uploaded files through a URL |
finfo requires the fileinfo extension, which is enabled by default. For images, re-encoding them (for example with the GD or Imagick extension) removes hidden content and metadata, adding another layer of safety.
Serving Stored Files
Files stored outside the web root are delivered through a PHP script that checks permissions and sends the file:
<?php
// avatar.php?file=3f9a1c0e8b7d4a2e9c6b1f0a5d8e7c42.jpg
$file = basename($_GET['file'] ?? '');
$path = __DIR__ . '/../storage/avatars/' . $file;
if (!preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $file) || !is_file($path)) {
http_response_code(404);
exit;
}
header('Content-Type: ' . (new finfo(FILEINFO_MIME_TYPE))->file($path));
header('X-Content-Type-Options: nosniff');
readfile($path);
The strict pattern accepts only file names in the format your upload code creates, which prevents path traversal. If uploads must be stored inside the public folder, configure the web server to never execute scripts in that folder.
Multiple Files
With name="photos[]" and the multiple attribute, users can select several files:
<input type="file" name="photos[]" multiple>
PHP organizes the result by property, not by file:
$_FILES['photos'] = [
'name' => ['beach.jpg', 'sunset.png'],
'type' => ['image/jpeg', 'image/png'],
'tmp_name' => ['/tmp/phpA1', '/tmp/phpB2'],
'error' => [0, 0],
'size' => [52341, 81022],
];
A helper converts this into one array per file:
<?php
function normalizeFiles(array $files): array
{
if (!is_array($files['name'] ?? null)) {
return [$files];
}
$normalized = [];
foreach (array_keys($files['name']) as $index) {
foreach ($files as $property => $values) {
$normalized[$index][$property] = $values[$index];
}
}
return $normalized;
}
$example = [
'name' => ['beach.jpg', 'sunset.png'],
'type' => ['image/jpeg', 'image/png'],
'tmp_name' => ['/tmp/phpA1', '/tmp/phpB2'],
'error' => [0, 0],
'size' => [52341, 81022],
];
foreach (normalizeFiles($example) as $file) {
echo "{$file['name']}: {$file['size']} bytes, error {$file['error']}\n";
}
Output
beach.jpg: 52341 bytes, error 0
sunset.png: 81022 bytes, error 0
Each normalized file can then be passed to the same validation function used for single uploads. Limit the number of files you accept; PHP's max_file_uploads setting (default 20) is an upper bound.
Configuration Limits
php.ini Setting | Default | Meaning |
|---|---|---|
file_uploads | On | Enables uploads |
upload_max_filesize | 2M | Maximum size per file |
post_max_size | 8M | Maximum size of the whole request; must be larger than upload_max_filesize |
max_file_uploads | 20 | Maximum number of files per request |
upload_tmp_dir | System default | Temporary folder |
If the whole request exceeds post_max_size, both $_FILES and $_POST are empty — the error codes above are never set. Check for this case explicitly, for example when REQUEST_METHOD is POST but $_FILES is empty. Web servers have their own limits too (such as Nginx's client_max_body_size).
Common Mistakes
Forgetting enctype="multipart/form-data"
Effect: $_FILES is empty. Fix: Add the attribute to the form.
Trusting name, type, or the Extension
Effect: Attackers upload executable scripts disguised as images. Fix: Detect the MIME type with finfo and choose the extension from an allow-list.
Storing Uploads in a Public, Executable Folder
Effect: Uploaded scripts can be executed through a URL. Fix: Store outside the web root, or disable script execution in the upload folder.
Keeping the Original File Name
Effect: Overwritten files, path traversal, and leaked personal information in names. Fix: Generate random names; store the original name in the database if you need to display it.
Using copy() or rename() Instead of move_uploaded_file()
Effect: No check that the file came from an upload. Fix: Always use move_uploaded_file().
Best Practices
- Check the error code, size, and real MIME type of every upload.
- Generate server-side file names and use extensions from an allow-list.
- Store uploads outside the web root and serve them through a controlled script.
- Normalize multi-file uploads and validate each file individually.
- Align
upload_max_filesize,post_max_size, and web server limits with your application's needs. Official reference: https://www.php.net/manual/en/reserved.variables.files.php