En bref : pour la sécurité des uploads PHP, ce n'est pas un contrôle isolé qui décide, mais la combinaison : vérifier le type de fichier côté serveur, renommer les fichiers, les stocker hors de la racine web et n'autoriser aucune exécution de scripts dans le dossier d'upload. Un formulaire d'upload non vérifié est l'une des voies classiques par lesquelles des attaquants installent une porte dérobée sur un serveur web.
Pourquoi les formulaires d'upload sont un point d'entrée privilégié
Chaque formulaire qui accepte des fichiers écrit des données étrangères sur votre serveur. Cela inclut les formulaires de candidature, les portails clients, les galeries d'images, les espaces de support et de nombreuses extensions de CMS et de boutiques. Si un attaquant parvient à y déposer un fichier que le serveur web exécute ensuite comme du PHP, il dispose d'une porte dérobée durable.
Pour la direction, cela signifie : un téléversement de CV d'apparence anodine peut, dans le pire des cas, conduire à la prise de contrôle de l'ensemble du serveur web. En outre, les dossiers d'upload contiennent souvent des données personnelles qui, en cas de mauvaise configuration, sont accessibles publiquement.
Pour les admins et les développeurs, cela signifie : le code applicatif et la configuration du serveur doivent s'accorder. Une faiblesse à un niveau est souvent rattrapée par l'autre, si les deux sont correctement mis en œuvre. Le guide Détecter et supprimer les webshells décrit comment trouver des fichiers déjà introduits.
Les règles de base de la sécurité des uploads PHP
Les règles suivantes correspondent aux recommandations des OWASP Cheat Sheets et ont fait leurs preuves en exploitation :
- Liste d'autorisation plutôt que liste de blocage : n'accepter que des types de fichiers expressément autorisés, par exemple PDF, JPG et PNG. Les listes de blocage pour des extensions isolées se contournent par des variantes et des doubles extensions.
- Vérifier le contenu, pas seulement l'extension : déterminer le type réel côté serveur avec
finfo. Le type MIME dans$_FILESest une indication de l'expéditeur et n'est pas fiable. - Nouveau nom de fichier : ne jamais utiliser le nom d'origine comme nom de fichier. Générer un nom aléatoire et ajouter l'extension issue de la liste d'autorisation.
- Stockage hors de la racine web : enregistrer les fichiers de sorte qu'ils ne soient pas directement accessibles par URL. La livraison se fait via un script avec contrôle des autorisations.
- Limiter la taille : dans l'application, dans PHP et dans le serveur web.
- Aucune exécution : si les uploads doivent malgré tout se trouver dans la racine web, désactiver l'exécution de scripts dans ce répertoire.
- Contrôle uniquement sur le serveur : les contrôles JavaScript dans le navigateur améliorent l'utilisation, mais ne protègent pas.
Un exemple sûr avec finfo et un nom aléatoire
L'exemple suivant montre le cœur d'un traitement sûr. Il est volontairement bref et doit être intégré à votre application, donc avec authentification, contrôle des autorisations, protection CSRF et journalisation.
<?php
declare(strict_types=1);
const MAX_BYTES = 5 * 1024 * 1024; // 5 Mo
const ERLAUBT = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // hors de la racine web
$datei = $_FILES['dokument'] ?? null;
if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Upload fehlgeschlagen.');
}
if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
http_response_code(400);
exit('Datei ungültig.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ = $finfo->file($datei['tmp_name']);
if (!isset(ERLAUBT[$typ])) {
http_response_code(415);
exit('Dateityp nicht erlaubt.');
}
$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;
if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
http_response_code(500);
exit('Speichern fehlgeschlagen.');
}
chmod($ziel, 0640);
// Enregistrer le nom d'origine uniquement comme texte dans la base de données
// et l'échapper avec htmlspecialchars() à chaque affichage.
Pour les images, une étape supplémentaire vaut la peine : réencoder l'image avec GD ou Imagick en supprimant les métadonnées. Cela ne remplace pas les autres contrôles, mais réduit le risque résiduel dû à des données étrangères intégrées.
Pour les documents de sources inconnues, par exemple des candidatures ou des photos de réclamation, une analyse antivirus avant transmission est en outre judicieuse. Elle protège moins le serveur web que les collaborateurs qui ouvriront ensuite les fichiers sur leurs postes de travail. Journalisez aussi chaque upload avec l'heure, le compte, la taille du fichier et le type détecté, afin de pouvoir retracer plus tard des schémas inhabituels.
Pour les CMS et les boutiques, les médiathèques intégrées apportent généralement leurs propres contrôles. Plus risquées sont les extensions de formulaires, les fonctions d'import et les pages complémentaires programmées sur mesure qui enregistrent des fichiers en contournant la bibliothèque. Ce sont précisément ces endroits que vous devriez examiner en premier lors d'un contrôle.
Livrez les fichiers du dossier protégé avec Content-Disposition: attachment et X-Content-Type-Options: nosniff. Le navigateur n'affiche alors pas directement les contenus problématiques, mais les propose en téléchargement.
Désactiver l'exécution de PHP dans le dossier d'upload
Même si un fichier au contenu falsifié passe entre les mailles, le serveur web ne doit pas l'exécuter comme un script. Cette deuxième couche de protection a sa place sur tout serveur qui stocke des uploads dans la racine web.
Pour Nginx avec PHP-FPM :
location ^~ /uploads/ {
location ~* \.(php|phtml|phar)$ {
deny all;
}
}
Pour Apache, de préférence directement dans le VirtualHost plutôt que dans un .htaccess, afin que la règle ne puisse pas être écrasée par l'application :
<Directory "/var/www/html/uploads">
AllowOverride None
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>
</Directory>
Si PHP tourne comme module Apache (mod_php), vous pouvez en plus définir php_admin_flag engine off dans le même bloc. Après la modification, vérifiez avec un fichier de test inoffensif, qui ne fait qu'afficher un texte, que le serveur répond 403 à une requête sur ce fichier.
Limites de taille et lien avec PHP2Shell
Les uploads sont étroitement liés aux réglages PHP post_max_size et upload_max_filesize. Dans le cadre de PHP2Shell, php.net recommande comme mesure transitoire de fixer post_max_size à 8M lorsqu'aucun upload n'est nécessaire, ou à 32M avec uploads, et de maintenir upload_max_filesize en dessous. Ce n'est pas une protection complète et cela ne remplace pas la mise à jour vers une version de PHP corrigée.
; php.ini ou pool FPM (php_admin_value), puis redémarrer tous les workers
file_uploads = On
post_max_size = 32M
upload_max_filesize = 20M
max_file_uploads = 5
Si un site n'a besoin d'aucun upload, file_uploads = Off est la solution la plus propre. Les réglages judicieux d'ici au correctif figurent sous Mesures d'urgence sans correctif. Les droits adaptés pour les répertoires d'upload sont décrits dans Droits des fichiers sur le serveur web.
Erreurs typiques dans les fonctions d'upload
Lors de l'examen d'applications existantes, les mêmes schémas reviennent sans cesse. Vérifiez vos formulaires sur ces points de façon ciblée :
- L'extension n'est vérifiée que partiellement, de sorte que des noms de fichiers à plusieurs extensions passent.
- Le nom d'origine du formulaire se retrouve dans le chemin de stockage. Cela ouvre la voie à la manipulation de chemins et à l'écrasement de fichiers existants.
- Les uploads sont publics avec des noms prévisibles. Pour des candidatures ou des contrats, c'est aussi un problème de protection des données.
- Le dossier d'upload est à la fois accessible en écriture et exécutable pour le serveur web.
- D'anciennes extensions d'upload ou des plugins de formulaire ne sont pas mis à jour alors que l'éditeur a publié des mises à jour de sécurité.
- Les erreurs ne sont pas journalisées. Or les uploads refusés sont un signal précoce de tentatives ciblées.
Comment PHP SOS aide-t-il concrètement ?
- Nous examinons vos fonctions d'upload dans le code et dans la configuration du serveur et nommons les faiblesses concrètes avec priorité.
- Nous mettons en place le blocage d'exécution dans le dossier d'upload, les droits adaptés et les limites de taille dans le cadre du renforcement de serveur.
- En cas de soupçon, nous fouillons les répertoires d'upload à la recherche de scripts introduits et les nettoyons via notre suppression de malwares.
- Sur demande, nous faisons recouper la mise en œuvre de façon indépendante par un test d'intrusion.
Questions fréquentes
Suffit-il de vérifier l'extension du fichier lors du téléversement ?
Non. L'extension et le type MIME signalé par le navigateur proviennent de l'expéditeur et peuvent être choisis librement. Vérifiez le contenu côté serveur, attribuez un nouveau nom de fichier et bloquez l'exécution de scripts dans le répertoire d'upload.
Où faut-il stocker les fichiers téléversés ?
De préférence en dehors du répertoire web public. La livraison est alors assurée par un script qui vérifie au préalable si l'utilisateur peut voir le fichier. Ainsi, les fichiers ne sont ni directement accessibles ni exécutables.
Les fichiers SVG sont-ils inoffensifs comme images ?
Pas automatiquement. Le SVG est du XML et peut contenir des contenus actifs. N'autorisez le SVG que si vous nettoyez les fichiers ou ne les livrez qu'en téléchargement, avec l'en-tête Content-Disposition approprié.
La taille des uploads a-t-elle un rapport avec PHP2Shell ?
Oui, indirectement. php.net recommande comme mesure transitoire des valeurs plus basses pour post_max_size et upload_max_filesize. Cela réduit le risque, mais ne remplace pas la mise à jour vers une version de PHP corrigée et le redémarrage de tous les workers.