PHPFile Uploads ($_FILES)

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

HTML
<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
<?php
print_r($_FILES['avatar']);
Array
(
    [name] => photo.jpg
    [type] => image/jpeg
    [tmp_name] => /tmp/phpA1b2C3
    [error] => 0
    [size] => 204800
)

Key

Meaning

name

The original filename as reported by the browser - not trustworthy.

type

The MIME type reported by the browser - also not trustworthy.

tmp_name

The path where PHP temporarily stored the uploaded file on the server.

size

The size of the uploaded file in bytes.

error

An error code; 0 (UPLOAD_ERR_OK) means the upload succeeded.

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
<?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
<?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.';
}
Never trust the client-supplied filename or MIME type
`$_FILES['avatar']['name']` and `['type']` are values the browser sends, and a browser only reports what the client-side software told it to. An attacker can upload a PHP script renamed to `photo.jpg` with `type` set to `image/jpeg`. Always verify the actual file content with a function like `mime_content_type()` or an image library, generate your own random filename rather than reusing the original one, and never execute or `include` a file from the upload directory.
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.

upload_max_filesize and post_max_size work together
Two `php.ini` directives cap uploads before your code even runs: `upload_max_filesize` limits a single file, and `post_max_size` limits the entire request body. If `post_max_size` is smaller than `upload_max_filesize`, large uploads can silently fail — keep `post_max_size` comfortably larger.
  • Add enctype="multipart/form-data" to any form with a file input.

  • Check $_FILES[...]["error"] === UPLOAD_ERR_OK before doing anything else.

  • Validate size and the real, detected MIME type - never the client-reported name or type.

  • Generate a new random filename instead of trusting the original one.

  • Store the file outside the web root, or otherwise prevent direct execution.

Tip
When accepting images specifically, re-encoding the upload through an image library (for example, loading and re-saving it with GD or Imagick) strips embedded scripts some malformed "image" files rely on, adding a further layer of safety beyond MIME-type checks.