File Uploads ($_FILES)
Uploading a file works differently from a normal text field, both in the HTML form and in how PHP exposes the data. Getting the form markup right and understanding the shape of $_FILES are the two prerequisites before any of the validation and storage logic makes sense.
The form needs multipart/form-data
By default, browsers encode form submissions as plain text, which cannot carry binary file contents. Adding enctype="multipart/form-data" to the <form> tag switches the browser to an encoding that can embed file bytes alongside regular fields. Forgetting this attribute is the most common reason $_FILES shows up empty.
An upload form
<form method="post" action="upload.php" enctype="multipart/form-data"> <label for="avatar">Profile picture</label> <input type="file" id="avatar" name="avatar" accept="image/png, image/jpeg"> <button type="submit">Upload</button> </form>
The shape of $_FILES
For an input named avatar, PHP populates $_FILES['avatar'] as an associative array describing the uploaded file. Every key is worth understanding before you write any upload-handling code.
Inspecting $_FILES after a submission
<?php print_r($_FILES['avatar']);
Array
(
[name] => photo.jpg
[type] => image/jpeg
[tmp_name] => /tmp/phpA1b2C3
[error] => 0
[size] => 204800
)Key | Meaning |
|---|---|
| The original filename as reported by the browser - not trustworthy. |
| The MIME type reported by the browser - also not trustworthy. |
| The path where PHP temporarily stored the uploaded file on the server. |
| The size of the uploaded file in bytes. |
| An error code; |
Moving the file with move_uploaded_file()
PHP stores every uploaded file in a temporary location that gets deleted at the end of the request. To keep it, you must move it yourself with move_uploaded_file(), which also verifies that the source path really was created by an upload (protecting against a crafted tmp_name value).
Moving an uploaded file to permanent storage
<?php
$file = $_FILES['avatar'] ?? null;
if ($file !== null && $file['error'] === UPLOAD_ERR_OK) {
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = '/var/app-storage/uploads/' . $newFilename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
echo 'Upload saved.';
} else {
echo 'Failed to save the uploaded file.';
}
}Upload saved.
Validating size, extension, and real MIME type
The size, name, and type values in $_FILES are useful hints, but the extension and MIME type are exactly what the browser reported — they are trivial for a visitor to fake by renaming a file or editing the request. Checking the file's actual content, not just its claimed type, is what makes validation meaningful.
Checking size and the real MIME type
<?php
$file = $_FILES['avatar'];
$maxBytes = 2 * 1024 * 1024; // 2 MB
$allowedMimeTypes = ['image/jpeg', 'image/png'];
$errors = [];
if ($file['size'] > $maxBytes) {
$errors[] = 'File is too large (max 2 MB).';
}
$actualMimeType = mime_content_type($file['tmp_name']);
if (!in_array($actualMimeType, $allowedMimeTypes, true)) {
$errors[] = 'Only JPEG and PNG images are allowed.';
}Store uploads outside the web root
If uploaded files live inside a directory the web server serves directly, a successfully disguised malicious file could be requested and executed by anyone who guesses or discovers its URL. Storing uploads in a directory outside the document root — and serving them back through a PHP script that checks permissions and streams the file — removes that direct-execution risk entirely.
Add
enctype="multipart/form-data"to any form with a file input.Check
$_FILES[...]["error"] === UPLOAD_ERR_OKbefore doing anything else.Validate size and the real, detected MIME type - never the client-reported
nameortype.Generate a new random filename instead of trusting the original one.
Store the file outside the web root, or otherwise prevent direct execution.