Situation actuelle : PHP2Shell. Deux failles PHP critiques (CVSS 9.2) permettent d'exécuter du code malveillant sans authentification. Correctifs disponibles depuis le 21 octobre 2026.Ce qu'il faut faire maintenant →

Comment PHP traite les uploads de fichiers

Entre le clic sur « Téléverser » et votre code, PHP exécute toute une chaîne de traitement. Cette page explique multipart/form-data, les fichiers temporaires, $_FILES, les limites et le lien avec PHP2Shell.

État 6 min de lectureSavoir PHP

En bref : lorsqu'un navigateur téléverse un fichier, il envoie une requête POST au format multipart/form-data, dans laquelle les champs du formulaire et le contenu des fichiers se suivent sous forme de parties distinctes. PHP décompose lui-même cette requête, écrit chaque partie de fichier dans un fichier temporaire du upload_tmp_dir, consigne les informations dans $_FILES et ne démarre votre script qu'ensuite. Le script déplace le fichier avec move_uploaded_file() ; tout ce qui n'est pas déplacé est supprimé par PHP à la fin de la requête.

Le trajet d'un fichier, du navigateur jusqu'au script

Pour les décideurs, un point est essentiel : un upload n'est pas une opération qui concerne la seule application. Une grande partie du travail s'effectue dans l'interpréteur PHP, avant même que le code du CMS ou de la boutique n'intervienne. C'est pourquoi les failles de cette partie de PHP touchent chaque site web, qu'il dispose ou non d'un formulaire d'upload.

Le déroulement en un coup d'œil :

  1. Le formulaire avec method="POST" et enctype="multipart/form-data" est envoyé.
  2. Le serveur web accepte la requête et la transmet à PHP, selon le mode de fonctionnement via PHP-FPM, mod_php ou une autre interface.
  3. PHP vérifie les limites telles que post_max_size et découpe le contenu à l'aide d'une chaîne de séparation, appelée boundary.
  4. Les champs normaux se retrouvent dans $_POST ; les parties de fichiers sont écrites dans des fichiers temporaires et décrites dans $_FILES.
  5. Ce n'est qu'à ce moment que le script démarre, contrôle le fichier et le déplace vers sa destination.
  6. À la fin de la requête, PHP supprime tous les fichiers temporaires qui n'ont pas été déplacés.

multipart/form-data : la structure d'une requête d'upload

Une requête d'upload est du texte brut contenant des données de fichier intégrées. L'en-tête Content-Type indique la boundary, et dans le contenu, cette chaîne sépare les différentes parties. Chaque partie possède ses propres en-têtes avec le nom du champ et, le cas échéant, le nom du fichier. Un exemple inoffensif, fortement abrégé :

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--

Toutes les informations de ce texte proviennent de l'expéditeur, y compris le nom du fichier et le Content-Type de la partie. PHP les reprend dans $_FILES, mais n'en vérifie pas la vraisemblance.

Le tableau $_FILES

Pour un champ nommé lebenslauf, $_FILES['lebenslauf'] contient, selon le manuel PHP, les clés suivantes :

Clé Contenu Digne de confiance ?
name nom d'origine du fichier chez l'expéditeur non
full_path chemin communiqué par le navigateur, depuis PHP 8.1.0 non
type type MIME selon le navigateur, non vérifié par PHP non
size taille en octets déterminée par PHP
tmp_name chemin du fichier temporaire sur le serveur attribué par PHP
error code d'erreur du traitement de l'upload défini par PHP

Pour les champs dont le nom contient des crochets, par exemple bilder[], chaque clé contient un tableau avec une entrée par fichier. L'application parcourt alors, par exemple, $_FILES['bilder']['error'] et accède à tmp_name et name par le même index.

Un formulaire peut en outre envoyer un champ caché MAX_FILE_SIZE avant le champ de fichier. Selon le manuel, il ne s'agit que d'une indication pour le navigateur, facile à contourner. Vous ne pouvez compter que sur les limites imposées par le serveur : dans PHP, dans le serveur web et dans l'application elle-même.

Limites : les directives php.ini pour les uploads

Ces directives pilotent le traitement des uploads. Les valeurs par défaut proviennent du manuel PHP et s'appliquent lorsqu'aucun php.ini ne les remplace.

Directive Valeur par défaut Modifiable dans
file_uploads 1 configuration système uniquement
upload_tmp_dir vide (répertoire temporaire du système) configuration système uniquement
upload_max_filesize 2M php.ini, pool, configuration de répertoire
post_max_size 8M php.ini, pool, configuration de répertoire
max_file_uploads 20 php.ini, pool, configuration de répertoire
enable_post_data_reading 1 php.ini, pool, configuration de répertoire

post_max_size doit être supérieur à upload_max_filesize, et memory_limit devrait en règle générale, selon le manuel, être supérieur à post_max_size. Si upload_tmp_dir n'est pas accessible en écriture, PHP se rabat sur le répertoire temporaire du système. Avec open_basedir, le répertoire temporaire doit être autorisé, sinon les uploads échouent. Les valeurs pratiques et leurs conséquences sont décrites dans l'article Limiter judicieusement post_max_size et upload_max_filesize.

Évaluer correctement les codes d'erreur

Le champ error indique ce qui s'est passé pendant le traitement. Les constantes vont de UPLOAD_ERR_OK (0) à UPLOAD_ERR_EXTENSION (8), en passant par UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3), UPLOAD_ERR_NO_FILE (4), UPLOAD_ERR_NO_TMP_DIR (6) et UPLOAD_ERR_CANT_WRITE (7).

<?php
declare(strict_types=1);

$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
    exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');   // Aucune donnée d'upload, post_max_size a peut-être été dépassé.
}

$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);
}

L'exemple ne montre que la mécanique. Le contrôle du type de fichier, le stockage en dehors de la racine web et le blocage de l'exécution de scripts sont décrits dans le guide Mettre en œuvre les uploads de fichiers PHP en toute sécurité.

Uploads et PHP2Shell : ce que signifient les mesures transitoires

Les failles PHP2Shell CVE-2026-109618 et CVE-2026-109619 concernent le traitement des requêtes dans l'interpréteur PHP. C'est pourquoi php.net cite, parmi les mesures transitoires, notamment post_max_size = 8M (sans uploads) ou 32M (avec uploads), upload_max_filesize en dessous, ainsi que session.upload_progress.enabled=0. Ces valeurs relèvent du php.ini, du pool FPM ou de la configuration du serveur ; tous les workers doivent ensuite redémarrer.

Des auteurs spécialisés proposent en outre de bloquer au niveau du proxy ou du WAF les requêtes multipart/form-data en dehors des véritables chemins d'upload. Il s'agit d'une mesure non officielle, avec le risque de casser des formulaires ; l'article Filtrer les requêtes d'upload en périphérie décrit les compromis. Aucune de ces mesures ne remplace la mise à jour vers PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35.

Comment PHP SOS aide-t-il concrètement ?

  • Nous déterminons lesquels de vos sites ont réellement besoin d'uploads, et sur quels chemins.
  • Nous définissons les limites et les mesures transitoires pool par pool, redémarrons tous les workers et testons les formulaires.
  • Nous recherchons des fichiers exécutables dans les répertoires d'upload et bloquons l'exécution de scripts dans le cadre du durcissement de serveur.
  • Nous installons la mise à jour de PHP et documentons le niveau de correctifs de chaque site.

Questions fréquentes

Où PHP enregistre-t-il d'abord les fichiers téléversés ?

Dans un répertoire temporaire. Par défaut, il s'agit du répertoire temporaire du système ; upload_tmp_dir permet de le modifier. Si le script ne déplace pas le fichier, PHP le supprime à la fin de la requête.

Puis-je me fier au champ type de $_FILES ?

Non. Le type MIME est communiqué par le navigateur, et selon le manuel PHP ne le vérifie pas. name et full_path proviennent eux aussi de l'expéditeur. Le type réel du fichier se détermine côté serveur, à partir du contenu.

Pourquoi $_FILES est-il vide alors qu'un fichier a été envoyé ?

Le plus souvent, la requête entière dépassait post_max_size. PHP vide alors entièrement $_POST et $_FILES, sans code d'erreur dans le tableau. D'autres causes possibles : un enctype manquant dans le formulaire ou file_uploads désactivé.

Interdire les uploads dans l'application protège-t-il contre PHP2Shell ?

Non. Le traitement des données de la requête a lieu dans PHP, avant l'exécution du code de l'application. Ce qui est efficace, c'est la mise à jour de PHP et le redémarrage de tous les workers ; des limites plus strictes ne sont qu'une mesure transitoire.

Quelle est la différence entre upload_max_filesize et post_max_size ?

upload_max_filesize limite chaque fichier pris isolément, post_max_size l'ensemble du contenu de la requête, fichiers et champs compris. post_max_size doit donc être supérieur à upload_max_filesize.

Sources

Contact immédiat

Chaque heure compte. Écrivez-nous maintenant.

Décrivez brièvement votre système, votre version de PHP et ce que vous avez observé. Nous vous répondons, faisons le point et travaillons à distance jusqu'à la clôture et la documentation de l'incident.

Aide immédiate