Current situation: PHP2Shell. Two critical PHP vulnerabilities (CVSS 9.2) allow malicious code without login. Patches since 21 October 2026.What to do now →

How PHP Processes File Uploads

Between the click on Upload and your code lies a whole processing chain inside PHP. This page explains multipart/form-data, temporary files, $_FILES, limits and the link to PHP2Shell.

Last updated 5 min readPHP knowledge

In short: When a browser uploads a file, it sends a POST request in the multipart/form-data format, in which form fields and file contents appear one after another as separate parts. PHP splits this request itself, writes each file part to a temporary file in upload_tmp_dir, records the details in $_FILES and only then starts your script. The script moves the file with move_uploaded_file(); everything that is not moved is deleted by PHP at the end of the request.

The path of a file from the browser to the script

For decision-makers, one thing matters above all: an upload is not an operation of the application alone. A large part of the work happens in the PHP interpreter before the code of the CMS or shop is even involved. That is why flaws in this part of PHP affect every website, whether or not it has an upload form.

The sequence at a glance:

  1. The form with method="POST" and enctype="multipart/form-data" is submitted.
  2. The web server accepts the request and passes it on to PHP, depending on the operating mode via PHP-FPM, mod_php or another interface.
  3. PHP checks limits such as post_max_size and splits the content using a separator string, the so-called boundary.
  4. Normal fields end up in $_POST; file parts are written to temporary files and described in $_FILES.
  5. Only now does the script start, check the file and move it to its destination.
  6. At the end of the request, PHP deletes all temporary files that were not moved.

multipart/form-data: the structure of an upload request

An upload request is plain text with embedded file data. The Content-Type header names the boundary, and within the body this character string separates the individual parts. Each part has its own header lines with the field name and, where applicable, the file name. A heavily shortened, harmless example:

POST /bewerbung HTTP/1.1
Content-Type: multipart/form-data; boundary=----trenner42

------trenner42
Content-Disposition: form-data; name="vorname"

Erika
------trenner42
Content-Disposition: form-data; name="lebenslauf"; filename="lebenslauf.pdf"
Content-Type: application/pdf

%PDF-1.7 ... Dateiinhalt ...
------trenner42--

All details in this text come from the sender, including the file name and the Content-Type of the part. PHP adopts them into $_FILES but does not check them for plausibility.

The $_FILES array

For a field named lebenslauf, $_FILES['lebenslauf'] contains these keys according to the PHP manual:

Key Content Trustworthy?
name original file name at the sender no
full_path path reported by the browser, since PHP 8.1.0 no
type MIME type according to the browser, not checked by PHP no
size size in bytes determined by PHP
tmp_name path of the temporary file on the server assigned by PHP
error error code of the upload processing set by PHP

For fields with square brackets in the name, such as bilder[], each key contains an array with one entry per file. The application then iterates over, for example, $_FILES['bilder']['error'] and accesses tmp_name and name through the same index.

A form can additionally send a hidden MAX_FILE_SIZE field before the file field. According to the manual, this is only a hint to the browser and is easy to bypass. You can rely solely on the limits that the server enforces: in PHP, in the web server and in the application itself.

Limits: the php.ini directives for uploads

These directives control upload processing. The default values come from the PHP manual and apply when no php.ini overrides them.

Directive Default Changeable in
file_uploads 1 system configuration only
upload_tmp_dir empty (system temp) system configuration only
upload_max_filesize 2M php.ini, pool, directory configuration
post_max_size 8M php.ini, pool, directory configuration
max_file_uploads 20 php.ini, pool, directory configuration
enable_post_data_reading 1 php.ini, pool, directory configuration

post_max_size must be larger than upload_max_filesize, and according to the manual memory_limit should generally be larger than post_max_size. If upload_tmp_dir is not writable, PHP falls back to the system's temp directory. With open_basedir, the temp directory must be allowed, otherwise uploads fail. Practical values and their consequences are described in the article Setting sensible limits for post_max_size and upload_max_filesize.

Evaluating error codes correctly

The error field tells you what happened during processing. The constants range from UPLOAD_ERR_OK (0) through UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) and UPLOAD_ERR_NO_FILE (4) to UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) and UPLOAD_ERR_EXTENSION (8).

<?php
declare(strict_types=1);

$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
    exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');
}

$meldung = match ($datei['error']) {
    UPLOAD_ERR_OK => null,
    UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Datei zu groß.',
    UPLOAD_ERR_NO_FILE => 'Keine Datei gewählt.',
    default => 'Upload fehlgeschlagen.',
};

if ($meldung === null && is_uploaded_file($datei['tmp_name'])) {
    $ziel = '/srv/app/uploads/' . bin2hex(random_bytes(16)) . '.pdf';
    move_uploaded_file($datei['tmp_name'], $ziel);
}

The example shows only the mechanics. Checking the file type, storing files outside the web root and blocking script execution are described in the guide Implementing file uploads in PHP securely.

Uploads and PHP2Shell: what the interim measures mean

The PHP2Shell vulnerabilities CVE-2026-109618 and CVE-2026-109619 affect the processing surrounding the request in the PHP interpreter. That is why php.net names, among other interim measures, post_max_size = 8M (without uploads) or 32M (with uploads), upload_max_filesize below that, and session.upload_progress.enabled=0. These values belong in php.ini, the FPM pool or the server configuration, after which all workers must restart.

Specialist authors additionally suggest blocking multipart/form-data requests outside genuine upload paths at the proxy or in the WAF. This is an unofficial measure with the risk that forms break; the trade-off is described in the article Filtering upload requests at the edge. None of these measures replaces the update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35.

How does PHP SOS help?

  • We determine which of your websites actually need uploads and on which paths.
  • We set the limits and interim measures per pool, restart all workers and test the forms.
  • We check upload directories for executable files and block script execution as part of server hardening.
  • We apply the PHP update and document the patch status for each website.

Frequently asked questions

Where does PHP store uploaded files first?

In a temporary directory. By default this is the system's temporary directory; you can change it with upload_tmp_dir. If the script does not move the file, PHP deletes it at the end of the request.

Can I trust the type field in $_FILES?

No. The browser reports the MIME type, and according to the manual PHP does not check it. The name and full_path values also come from the sender. You determine the actual file type on the server from the content.

Why is $_FILES empty although a file was sent?

Usually the entire request was larger than post_max_size. In that case PHP empties $_POST and $_FILES completely, without an error code in the array. Other causes are a missing enctype in the form or file_uploads being switched off.

Does it help against PHP2Shell to forbid uploads in the application?

No. The request data is processed in PHP before any application code runs. What works is the PHP update and the restart of all workers; tighter limits are only an interim measure.

What is the difference between upload_max_filesize and post_max_size?

upload_max_filesize limits each individual file, post_max_size limits the entire content of the request including all files and fields. post_max_size must therefore be larger than upload_max_filesize.

Sources

Immediate contact

Every hour counts. Write to us now.

Briefly describe your system, PHP version and observation. We will get back to you, clarify the situation and work remotely until the incident is closed and documented.

Immediate help