Image de couverture : Uploads de fichiers sécurisés : 7 vérifications indispensables et pourquoi elles existent
tech

Uploads de fichiers sécurisés : 7 vérifications indispensables et pourquoi elles existent

06 May 2026
3 min de lecture
20 vues
Sébastien Muler

Uploads de fichiers sécurisés : 7 vérifications indispensables et pourquoi elles existent

Un upload de fichier, c'est le moment précis où vous cédez le contrôle à un utilisateur non vérifié. Tout le reste — champs de formulaire, paramètres d'URL, données JSON — reste du texte que vous validez, assainissez et stockez. Un fichier, lui, c'est de la donnée binaire arbitraire, issue d'une source que vous ne pouvez pas authentifier, sur le point d'être écrite dans votre système de fichiers.

La plupart des tutoriels couvrent le cas nominal : accepter le fichier, le stocker, retourner une URL. Le problème ne vient jamais du cas nominal.

Renommez un fichier PHP en image.jpg. Uploadez-le. De nombreuses applications l'acceptent — parce que getClientOriginalExtension() retourne jpg et getClientMimeType() retourne image/jpeg. Deux valeurs fournies par le client. Aucune vérifiée côté serveur.

C'est ainsi que les webshells s'installent. C'est ainsi que des XSS stockés se glissent dans des SVG. C'est le vide que cette architecture vient combler.


Pourquoi une seule vérification ne suffit pas

Un attaquant déterminé sonde chaque couche indépendamment. La défense en profondeur consiste à supprimer les chemins faciles à chaque niveau. C'est pourquoi il faut sept vérifications indépendantes, chacune fermant un vecteur d'attaque spécifique.

Aucune ne capte tout à elle seule. Ensemble, elles forment une surface d'attaque quasi impénétrable pour les vecteurs courants.


Les 7 vérifications, expliquées

1. Détection MIME côté serveur

Oubliez getClientMimeType() : c'est le navigateur qui parle, pas le fichier. La bonne approche consiste à lire les premiers octets du fichier (le magic number) pour identifier son vrai type.

$finfo = new \finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($file->getRealPath());

Un fichier PHP renommé en .jpg aura un magic number qui ne correspond pas à image/jpeg. La détection MIME réelle le détecte.

2. Extension sur liste blanche

Ne bloquez pas les extensions dangereuses — autorisez uniquement celles dont vous avez besoin. Toute extension non listée est rejetée par défaut.

$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'];
$extension = strtolower($file->getClientOriginalExtension());
if (!in_array($extension, $allowedExtensions)) {
    // Rejeter
}

3. Cohérence extension / MIME

Un fichier peut avoir la bonne extension ET le bon MIME déclaré, mais ils ne correspondent pas entre eux. Cette vérification croise les deux.

$mimeToExtension = [
    'image/jpeg' => ['jpg', 'jpeg'],
    'image/png'  => ['png'],
    'application/pdf' => ['pdf'],
];
if (!in_array($extension, $mimeToExtension[$detectedMime] ?? [])) {
    // Incohérence détectée
}

4. Limite de taille

La limite dans php.ini est un filet de sécurité serveur, pas une validation métier. Appliquez une limite explicite dans votre code, adaptée au contexte (avatar vs document).

$maxSize = 5 * 1024 * 1024; // 5 Mo
if ($file->getSize() > $maxSize) {
    // Trop volumineux
}

5. Validation du contenu image

Pour les images, une vérification supplémentaire s'impose : tenter de les re-traiter via GD ou Imagick. Si le fichier n'est pas une vraie image, cette opération échoue.

$imageInfo = @getimagesize($file->getRealPath());
if ($imageInfo === false) {
    // Ce n'est pas une image valide
}

Cette étape neutralise les images polyglotes — des fichiers qui sont à la fois une image valide et du code exécutable.

6. Détection de contenu malveillant

Rechercher des signatures de code dans le contenu du fichier : balises PHP, scripts JavaScript, directives SVG suspectes.

$content = file_get_contents($file->getRealPath());
$dangerousPatterns = ['<?php', '<?=', '<script', 'javascript:'];
foreach ($dangerousPatterns as $pattern) {
    if (stripos($content, $pattern) !== false) {
        // Contenu suspect
    }
}

7. Renommage systématique

Même si les 6 vérifications précédentes passent, ne conservez jamais le nom de fichier original. Générez un nom aléatoire. Cela casse les attaques par prédiction de chemin et empêche l'exécution directe via URL.

$safeName = bin2hex(random_bytes(16)) . '.' . $extension;
$file->storeAs('uploads', $safeName, 'private');

Stockez également en dehors de la racine web (storage/ plutôt que public/), et servez les fichiers via un contrôleur qui vérifie les droits d'accès.


Intégrer ces vérifications dans un middleware Symfony

L'approche middleware est idéale : centralisée, testable, réutilisable. En Symfony, un EventSubscriber sur KernelEvents::REQUEST ou un service dédié FileUploadValidator injecté dans vos contrôleurs accomplit le même rôle.

class FileUploadValidator
{
    public function validate(UploadedFile $file, array $allowedMimes): void
    {
        $this->checkRealMime($file, $allowedMimes);
        $this->checkExtension($file, $allowedMimes);
        $this->checkMimeExtensionConsistency($file);
        $this->checkSize($file);
        $this->checkImageContent($file);
        $this->checkMaliciousContent($file);
        // Le renommage intervient au moment du stockage
    }
}

Chaque méthode lève une exception typée en cas d'échec. Le contrôleur n'a pas à connaître les détails — il délègue et gère les exceptions.


Conclusion

Sécuriser les uploads de fichiers, c'est avant tout une question d'architecture. Une seule vérification donne une fausse impression de sécurité. Sept vérifications indépendantes, organisées en couches, transforment un point d'entrée risqué en un processus maîtrisé.

Chez MulerTech, nous appliquons systématiquement ce type de défense en profondeur dans nos projets Symfony. L'objectif n'est pas la complexité pour elle-même, mais la robustesse structurelle qui protège vos données et votre réputation sur le long terme.


Cet article est inspiré de Secure File Uploads: Seven Checks and Why Each One Exists par Shakil Alam, publié sur dev.to.

Partager cet article