Les fonctionnalités d’upload de fichiers sont omniprésentes dans les applications modernes. Photos de profil, factures, pièces d’identité ou médias reposent tous sur des mécanismes capables d’accepter des contenus fournis par des utilisateurs ou par des systèmes externes. Derrière cette fonctionnalité standard se cache pourtant une surface d’attaque particulièrement large.
Dans cet article, nous explorons les vulnérabilités d’upload de fichiers, les méthodes de détection et les techniques de contournement des contrôles. Nous détaillons également les exploitations et impacts ainsi que les méthodes à implémenter pour prévenir le risque.
Guide complet sur les failles d’upload de fichiers
- Qu’est-ce qu’une vulnérabilité d’upload de fichiers ?
- Comment fonctionne une fonctionnalité d’upload de fichiers ?
- Comment auditer une fonctionnalité d’upload de fichiers ?
- Cartographier tous les points d’upload et d’import
- Établir le comportement de référence
- Tester les couches de validation indépendamment
- Suivre le stockage, la restitution et les traitements
- Évaluer les contrôles d’accès et l’isolation multi-tenant
- Tester les traitements asynchrones et les consommateurs secondaires
- Techniques de contournement des contrôles d’upload
- Contourner la validation des extensions
- Contourner la validation du type MIME
- Contourner la validation des signatures et forger les magic bytes
- Différentiels de parsing et fichiers polyglottes
- Injections via le nom de fichier
- Path Traversal lors de l’upload
- Modification de la configuration serveur via des fichiers uploadés
- Contournements liés aux archives et extraction non sécurisée
- Abus de HTTP PUT et WebDAV
- Race conditions lors de l’upload
- Contourner la validation des extensions
- Exploiter les vulnérabilités d’upload de fichiers
- XSS stockée via des fichiers SVG et HTML
- Exécution de code à distance via l’upload de webshells
- Traitement malveillant de PDF et de documents
- Attaques XML via des fichiers uploadés
- SSRF via le traitement de fichiers
- Déni de service via l’upload de fichiers
- Contrôles d’accès défaillants, divulgation et écrasement de fichiers
- Écriture arbitraire de fichiers via l’extraction d’archives
- Formula Injection et contenus Office actifs
- Abus du stockage objet et des URLs présignées
- Comment sécuriser une fonctionnalité d’upload de fichiers ?
- N’autoriser que les types de fichiers nécessaires
- Valider les types MIME, les signatures et la structure des fichiers
- Authentifier et autoriser l’upload, le téléchargement et la modification
- Générer les noms de fichiers et les clés de stockage côté serveur
- Stocker les fichiers hors des emplacements web exécutables
- Appliquer le principe du moindre privilège
- Imposer des limites de taille, de nombre, de quota et de traitement
- Sécuriser l’extraction des archives
- Servir les fichiers uploadés de manière sûre
- Sécuriser le stockage objet et les workflows utilisant des URLs présignées
- Protéger les endpoints d’upload contre la CSRF
- Isoler les traitements de fichiers dans une sandbox
- Content Disarm and Reconstruction (CDR)
- Journalisation, supervision et alerting
- Conclusion
- Références
Qu’est-ce qu’une vulnérabilité d’upload de fichiers ?
Une vulnérabilité d’upload de fichiers apparaît lorsqu’une application autorise l’envoi de fichiers sans appliquer des contrôles suffisants sur leur validation, leur traitement, leur stockage ou leur restitution.
À première vue, l’opération semble simple : un utilisateur sélectionne un document ou une image et l’application l’enregistre. En pratique, chaque fichier uploadé constitue une donnée non fiable et doit être traité avec le même niveau de méfiance qu’un paramètre HTTP, un en-tête ou une valeur reçue par une API.
La difficulté vient de la richesse des fichiers. Au-delà de leur contenu visible, ils peuvent contenir des métadonnées, des scripts, des macros, du code interprétable, des objets embarqués ou des structures spécialement conçues pour provoquer un comportement inattendu dans un parser. Un fichier qui paraît inoffensif pendant la première phase de validation peut donc devenir dangereux lorsqu’un autre composant le traite plus tard.
Une vulnérabilité apparaît dès lors qu’un attaquant peut détourner cette chaîne de traitement afin d’effectuer une action non prévue par l’application. Selon le contexte, il peut chercher à faire accepter un fichier exécutable, injecter du contenu interprété par le navigateur, influencer un chemin de stockage, déclencher une vulnérabilité dans une bibliothèque de traitement, écraser un objet existant ou consommer de manière disproportionnée les ressources du serveur.
C’est pour cette raison que les fonctionnalités d’upload constituent une surface d’attaque importante. Une seule requête peut impliquer le serveur web, le framework applicatif, un stockage local ou cloud, un antivirus, une bibliothèque de traitement d’image, un parser PDF, un moteur OCR, une solution de conversion ou encore un CDN. Une faiblesse dans l’un de ces composants suffit parfois à transformer une fonctionnalité métier légitime en vecteur d’attaque.
Le problème fondamental n’est donc pas simplement qu’une application puisse accepter une extension dangereuse. Il réside dans le fait qu’un contenu contrôlé par l’utilisateur traverse plusieurs frontières de confiance et que les contrôles appliqués ne restent pas nécessairement cohérents tout au long de son cycle de vie.
Comment fonctionne une fonctionnalité d’upload de fichiers ?
Avant d’explorer les techniques de contournement, il est essentiel de comprendre ce qui se produit réellement lorsqu’un fichier est envoyé à une application web. Même si l’action paraît simple côté utilisateur, une requête d’upload traverse souvent plusieurs composants responsables de la réception, de la validation, du traitement, du stockage et de la diffusion du fichier. Chacune de ces étapes introduit de nouvelles hypothèses de sécurité.
Workflow classique d’un upload
Un upload classique débute lorsque l’utilisateur sélectionne un fichier et le soumet à l’application. Le navigateur encapsule alors le contenu dans une requête HTTP de type multipart/form-data.
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="document.pdf"
Content-Type: application/pdf
[file content]
------WebKitFormBoundary--
La requête transporte non seulement les octets du fichier, mais également plusieurs métadonnées contrôlables par le client, notamment le nom de fichier, le type MIME déclaré et d’éventuels paramètres additionnels. Tous ces éléments peuvent influencer la manière dont l’application valide et traite l’objet reçu.
Le fichier ne passe généralement pas directement du navigateur au stockage définitif. Le serveur web reçoit d’abord la requête et la transmet à l’application. Celle-ci applique une logique de validation, choisit une destination temporaire ou permanente puis peut déclencher d’autres traitements. Il est fréquent qu’un fichier soit ensuite analysé par un antivirus, redimensionné, converti, indexé, soumis à un OCR, débarrassé de certaines métadonnées ou transmis à un service tiers.
Ce n’est qu’après ces opérations que le contenu est éventuellement rendu accessible par une URL de téléchargement, une API, un espace utilisateur, un CDN ou un workflow interne.
La sécurité doit donc accompagner tout le parcours du fichier et pas seulement l’instant où celui-ci franchit l’endpoint d’upload.
Où sont appliqués les contrôles de sécurité ?
Une chaîne d’upload robuste combine plusieurs contrôles, car chacun ne couvre qu’une partie du risque.
| Étape de traitement | Contrôles de sécurité typiques |
| Validation côté client | Contrôles d’ergonomie uniquement, indications de taille et d’extension |
| Serveur web / gateway | Limites de taille des requêtes, restrictions de méthodes HTTP, routage de l’authentification |
| Couche applicative | Allowlist d’extensions, contrôle du MIME déclaré, politique de nommage, autorisation |
| Validation du contenu | Magic bytes, validation par parser, antivirus, CDR |
| Services de traitement | Sandboxing, limites de ressources, filtrage réseau sortant, parsers maintenus à jour |
| Stockage et restitution | Isolation, contrôle d’accès, stockage non exécutable, en-têtes HTTP sûrs |
La validation d’une extension ne contrôle que le nom. Le type MIME déclaré renseigne sur ce que le client prétend envoyer, mais cette valeur est elle-même contrôlée par l’utilisateur. Les signatures permettent d’identifier plus précisément un format, sans pour autant garantir que toute sa structure est saine. L’antivirus recherche quant à lui des motifs connus, alors que les contrôles de stockage déterminent qui pourra accéder au fichier une fois qu’il aura été accepté.
La sécurité dépend donc de la combinaison de ces mécanismes. Une application qui accepte invoice.jpg parce que l’extension est autorisée peut ensuite transmettre les octets à un composant qui détecte une structure différente et l’interprète d’une manière que la validation initiale n’avait jamais envisagée.
C’est une erreur fréquente de considérer qu’un fichier devient définitivement « sûr » dès qu’il a passé le premier contrôle. Chaque composant qui interagit avec le contenu doit appliquer des garanties adaptées à son propre contexte d’utilisation.
Comment auditer une fonctionnalité d’upload de fichiers ?
Un pentest efficace commence par modéliser la fonctionnalité comme un workflow complet plutôt que par tester immédiatement une liste de payloads. Il faut identifier ce que l’application pense contrôler, quel composant prend chaque décision et si les composants situés plus loin dans la chaîne interprètent le même fichier de la même façon.
L’approche la plus fiable consiste à modifier une propriété à la fois. Cette méthode permet de mettre en évidence les hypothèses de l’application et d’éviter d’attribuer un résultat à un contrôle alors que plusieurs paramètres ont changé simultanément.
Cartographier tous les points d’upload et d’import
Le formulaire visible n’est qu’un point d’entrée parmi d’autres. L’auditeur doit identifier les photos de profil, pièces jointes de support, espaces documentaires, imports de données, bibliothèques de médias, imports CSV, ingestion d’archives, traitements de pièces jointes reçues par e-mail, endpoints API, fonctionnalités mobiles et mécanismes permettant d’importer un fichier depuis une URL distante.
Les interfaces d’administration et les intégrations en arrière-plan sont particulièrement importantes. Elles traitent souvent des formats plus riches et peuvent s’exécuter avec davantage de privilèges que les fonctionnalités directement accessibles aux utilisateurs.
Pour chaque entrée, il est utile de relever les rôles autorisés, les formats attendus, le nombre de fichiers accepté, la taille maximale annoncée, la réponse renvoyée, l’existence d’un endpoint de finalisation et la manière dont le fichier est publié. Dans une architecture cloud, il faut également déterminer si l’application reçoit réellement les octets ou si elle génère uniquement un jeton temporaire ou une URL présignée permettant au client d’envoyer directement l’objet au stockage.
Établir le comportement de référence
Avant de modifier quoi que ce soit, il faut envoyer un fichier légitime et capturer l’échange complet. Une requête multipart/form-data révèle plusieurs valeurs qui pourront ensuite être testées indépendamment : le nom contenu dans Content-Disposition, le type MIME déclaré, les autres paramètres du formulaire et le corps binaire lui-même.
POST /api/files HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----Boundary
Cookie: session=...
------Boundary
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
%PDF-1.7
[file content]
------Boundary--
La réponse peut révéler que le fichier est renommé, qu’un identifiant d’objet est généré, qu’une URL de stockage est fournie ou qu’un traitement différé intervient après l’acceptation initiale. Une API de statut, une latence inhabituelle ou la création de fichiers dérivés peuvent également permettre d’identifier des consommateurs secondaires.
Tester les couches de validation indépendamment
Les tests les plus instructifs créent volontairement des désaccords entre les propriétés du fichier. L’auditeur peut conserver les octets tout en modifiant l’extension, garder l’extension et changer le Content-Type, préserver une signature attendue en modifiant le reste du contenu ou fournir un fichier syntaxiquement valide qui contient des structures inhabituelles.
Comparer la réponse immédiate et le comportement en aval aide à déterminer quelle propriété est réellement utilisée par chaque composant. Les restrictions côté navigateur doivent être considérées comme des contrôles d’ergonomie et non comme une frontière de sécurité, puisqu’il est toujours possible de rejouer directement la requête HTTP.
Les contrôles serveur doivent ensuite être testés pour les problèmes de normalisation, les blocklists incomplètes, les différences de parsing, les limites de taille incohérentes et les divergences entre l’endpoint d’upload et le service qui traite le fichier plus tard.
Suivre le stockage, la restitution et les traitements
Une fois le fichier accepté, il faut comprendre où il est envoyé. L’application renvoie-t-elle une URL ? Le contenu est-il servi directement par le serveur web, via un endpoint applicatif, depuis un CDN ou depuis un stockage objet ? Le nom original est-il conservé ? Le type MIME est-il recalculé ou simplement repris depuis la requête ? Le fichier est-il affiché inline ou forcé en téléchargement ? Le répertoire de stockage est-il exécutable par le serveur web ?
Les traitements secondaires doivent être déclenchés chaque fois que la fonctionnalité le permet : création de miniatures, aperçus de documents, OCR, extraction d’archives, lecture de métadonnées, conversion, analyse antivirus ou indexation. Une validation solide à l’entrée ne protège pas contre un worker vulnérable ou sur-privilégié situé plus loin dans la chaîne.
Évaluer les contrôles d’accès et l’isolation multi-tenant
La sécurité d’un fichier concerne aussi les opérations autorisées sur l’objet. Il faut tester la création, la lecture, le remplacement, le renommage, le partage et la suppression depuis des comptes ayant des rôles différents et, lorsque cela s’applique, appartenant à des organisations ou tenants différents.
Un cas fréquent est celui d’un endpoint d’upload correctement protégé suivi d’un endpoint de téléchargement ou de remplacement qui accepte un identifiant contrôlé par l’utilisateur sans vérifier l’autorisation sur l’objet ciblé.
GET /api/files/8d7d7db0 HTTP/1.1
Host: example.com
Cookie: session=userA
# Répéter la requête avec l’identifiant d’un fichier appartenant à userB
# ou à un autre tenant. L’autorisation doit être vérifiée sur chaque objet.
Les miniatures, versions converties, aperçus et copies mises en cache doivent hériter exactement du même modèle d’autorisation que le fichier original. Une copie dérivée exposée publiquement suffit à contourner une protection correcte sur l’original.
Tester les traitements asynchrones et les consommateurs secondaires
Les architectures modernes acceptent souvent le fichier avant de le traiter via une file de messages ou un worker. Plusieurs états intermédiaires apparaissent alors : en attente, en cours d’analyse, rejeté, converti ou publié. Il faut vérifier qu’un fichier n’est ni récupérable ni exécutable tant que tous les contrôles obligatoires ne sont pas terminés.
Il faut également vérifier ce qu’il se passe en cas d’échec. Un fichier rejeté est-il réellement supprimé de toutes les zones temporaires ? Une conversion interrompue laisse-t-elle un artefact accessible ? Une URL de téléchargement est-elle générée avant la fin du scan antivirus ? Ces fenêtres transitoires peuvent ouvrir la voie à des race conditions.
Enfin, le pentest doit tenir compte des consommateurs indirects. Un document peut être ouvert plus tard par un utilisateur du back-office, indexé, importé dans un ERP, envoyé par e-mail ou copié vers un second stockage. Ces chemins peuvent transformer une métadonnée ou une valeur apparemment anodine en XSS stockée, formula injection, exploitation de parser ou canal d’exfiltration.
Techniques de contournement des contrôles d’upload
Une fois le workflow compris, l’objectif consiste à déterminer comment les contrôles réagissent à des données inattendues. Il n’existe pas de technique universelle : l’attaquant cherche d’abord à savoir si l’application se fonde sur le nom, l’extension, le type MIME, les magic bytes, la structure du fichier, le chemin de stockage ou le moment auquel la validation intervient.
Le principe général est de créer un écart entre ce qui est contrôlé et ce qui sera ensuite interprété. Chaque test doit isoler autant que possible une seule hypothèse afin de comprendre exactement où se situe la faiblesse.
Contourner la validation des extensions
La vérification de l’extension est l’un des contrôles les plus répandus. Une application peut par exemple accepter avatar.jpg, invoice.pdf ou document.docx et refuser script.php, payload.jsp ou shell.aspx.
Ce mécanisme est utile, mais il devient fragile lorsqu’il est implémenté à partir d’une simple comparaison de chaînes. Le nom transmis dans Content-Disposition est entièrement contrôlé par le client et peut être modifié avant d’atteindre le serveur.
Content-Disposition: form-data; name="file"; filename="document.pdf"
Content-Type: application/pdf
Si la décision repose uniquement sur cette valeur, l’auditeur peut tester différentes représentations du nom afin d’identifier des incohérences de validation.
Doubles extensions
Une double extension consiste à ajouter une extension autorisée après une extension potentiellement dangereuse.
file.php.jpg
document.jsp.png
report.aspx.pdf
Cette technique vise les implémentations qui recherchent la présence d’une extension autorisée n’importe où dans le nom au lieu d’extraire et de contrôler strictement le suffixe final.
if (filename.includes(".jpg")) {
allowUpload();
}
file.php.jpg est alors accepté parce que la chaîne contient .jpg. Une implémentation robuste doit d’abord canonicaliser le nom, extraire l’extension finale selon une règle non ambiguë puis la comparer à une allowlist stricte.
Les doubles extensions deviennent particulièrement intéressantes lorsque plusieurs composants appliquent des règles différentes. La couche applicative peut examiner uniquement le dernier suffixe tandis qu’un serveur web, un reverse proxy, un convertisseur ou un handler legacy interprète le nom autrement. Il faut donc vérifier non seulement si le fichier est accepté, mais également comment l’objet final est traité.
Extensions exécutables alternatives
Une blocklist qui refuse uniquement .php, .asp ou .jsp peut oublier d’autres extensions interprétées par la plateforme ou par une configuration spécifique.
| Technologie | Exemples d’extensions potentiellement interprétées ou exécutables |
| PHP | .php, .php3, .php4, .php5, .phtml, .phar selon la configuration |
| ASP.NET | .aspx, .ashx, .asmx, .ascx |
| ASP classique | .asp et certains mappings historiques comme .asa ou .cer |
| Java | .jsp, .jspx, archives déployables comme .war dans certains workflows d’administration |
| ColdFusion | .cfm, .cfml, .cfc |
Une application peut ainsi refuser file.php tout en acceptant file.phtml si cette variante n’a pas été ajoutée à la blocklist. C’est précisément la raison pour laquelle une liste de formats explicitement autorisés est préférable à l’énumération de tous les formats considérés comme dangereux.
Manipulation de la casse
Une comparaison sensible à la casse peut créer des comportements incohérents.
file.php
file.PHP
file.PhP
file.pHp
Un contrôle comme celui-ci est fragile si la valeur n’est pas normalisée au préalable.
if (extension === "php") {
rejectUpload();
}
La bonne approche consiste à convertir l’extension vers une représentation canonique avant toute décision.
const extension = getExtension(filename).toLowerCase();
Ce contournement est rarement suffisant à lui seul dans les frameworks modernes, mais il reste utile pour détecter des validations artisanales ou incohérentes.
Points et espaces en fin de nom
Les points ou espaces terminaux peuvent provoquer des divergences entre la logique applicative et le système de fichiers.
avatar.jpg.
avatar.jpg
file.php.
file.php
Selon le système d’exploitation, le framework ou la couche de stockage, certains caractères peuvent être supprimés ou normalisés. Le nom contrôlé par l’application peut donc différer du nom effectivement stocké ou interprété.
Il faut aussi se méfier des expressions régulières ou comparaisons de chaînes mal ancrées qui tolèrent des espaces, caractères encodés ou transformations postérieures. Une politique robuste doit décoder, supprimer les caractères non nécessaires, normaliser puis canonicaliser le nom avant de prendre une décision.
Injection de null byte
L’injection de null byte est une technique historique qui cible les composants considérant le caractère nul comme un terminateur de chaîne.
file.php%00.jpg
L’application peut raisonner sur .jpg alors qu’un composant natif ou une bibliothèque ancienne interprète le nom comme file.php. Les environnements modernes protègent généralement contre ce comportement, mais la technique reste pertinente lorsqu’une application intègre du code natif, des extensions legacy ou des bibliothèques anciennes.
Au-delà de l’exploitation directe, ce test permet surtout de vérifier que les caractères de contrôle et les encodages sont rejetés avant la validation du nom.
Unicode et problèmes de canonicalisation
Unicode introduit des variantes visuellement proches mais techniquement distinctes. Un attaquant peut tester des homoglyphes, des caractères invisibles ou différentes formes de normalisation.
avatar.jpɡ
avatar.jg
avatar.jpg
Le danger apparaît lorsqu’un composant normalise la chaîne alors qu’un autre ne le fait pas. Une représentation peut passer le contrôle puis être transformée avant le stockage, l’affichage ou le traitement.
Une politique de nommage sûre définit les caractères réellement nécessaires, applique une normalisation Unicode cohérente, impose une longueur maximale et refuse les caractères invisibles ou ambigus lorsqu’ils n’ont aucune justification métier.
Contourner la validation du type MIME
Le type MIME contenu dans une requête multipart est fourni par le client et peut être modifié librement.
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
Un attaquant peut simplement remplacer la valeur par :
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: image/jpeg
Si l’application accepte le second envoi uniquement parce que Content-Type vaut image/jpeg, elle fait confiance à une métadonnée non fiable. Le MIME déclaré reste utile comme signal de cohérence, mais il doit être corrélé avec l’extension attendue, la signature binaire, la structure du fichier et la manière dont celui-ci sera utilisé.
Contourner la validation des signatures et forger les magic bytes
Les magic bytes correspondent aux premiers octets permettant d’identifier de nombreux formats.
| Type de fichier | Signature / magic bytes courants |
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
| 25 50 44 46 | |
| ZIP | 50 4B 03 04 |
Ce contrôle est plus solide qu’un type MIME déclaré par le client, mais il ne garantit pas à lui seul que tout le fichier est valide ou sûr. Un contenu peut commencer par une signature correcte et contenir ensuite des données inattendues, des objets actifs ou une structure destinée à un autre parser.
Le problème est celui de la différence entre identification et innocuité. Les magic bytes permettent d’affirmer qu’un fichier ressemble à un format, pas qu’il ne contient aucun comportement dangereux. Pour les formats sensibles, la validation doit s’appuyer sur un parser maintenu à jour, refuser les structures malformées ou ambiguës et, lorsque c’est possible, reconstruire ou réencoder le contenu dans une représentation canonique.
Différentiels de parsing et fichiers polyglottes
Un différentiel de parsing apparaît lorsque deux composants donnent une signification différente aux mêmes octets. Un fichier polyglotte illustre particulièrement bien ce principe : il est construit de manière à être accepté ou interprété de façon significative par plusieurs parsers.
L’exploitation ne nécessite pas toujours un fichier parfaitement valide au regard de deux spécifications. Il suffit parfois qu’un validateur tolère une structure ou des octets additionnels alors qu’un autre composant extrait une seconde interprétation exploitable. Des combinaisons historiques ont par exemple concerné des formats GIF/JAR ou PDF/ZIP, et certains formats d’images restent parfaitement lisibles malgré l’ajout de données en fin de fichier.
Lors d’un audit, la question importante n’est donc pas uniquement « quel est le type de ce fichier ? », mais « quels parsers vont le recevoir et sont-ils d’accord sur ce qu’il contient ? ». Le validateur d’upload, le moteur de preview, le serveur web, l’extracteur de métadonnées et le convertisseur peuvent avoir des comportements différents.
Injections via le nom de fichier
Le nom d’origine devient dangereux lorsqu’il est réutilisé dans un autre contexte sans traitement adapté. Cette valeur peut apparaître dans une page HTML, un log, une commande shell, une requête SQL, un en-tête HTTP ou un chemin local. Chacun de ces contextes possède ses propres règles d’encodage ou de paramétrage.
Content-Disposition: form-data; name="file"; filename="invoice.pdf"
# Exemples à tester uniquement dans un environnement autorisé :
"><svg onload=alert(1)>.jpg
../../invoice.pdf
report;id;.pdf
invoice%0d%0aX-Test: injected.pdf
Afficher le nom sans encodage HTML peut conduire à une XSS stockée. L’insérer dans une commande shell peut créer une injection de commandes. Le placer sans contrôle dans Content-Disposition ou un autre en-tête peut produire des problèmes d’injection d’en-têtes dans certains stacks.
La conception la plus sûre consiste à générer un identifiant interne pour le stockage et à conserver le nom original uniquement comme métadonnée. Même dans ce cas, cette métadonnée doit être normalisée, limitée en longueur et encodée en fonction du contexte dans lequel elle sera affichée ou renvoyée.
Path Traversal lors de l’upload
Un Path Traversal apparaît lorsque des données contrôlées par l’utilisateur influencent directement l’emplacement où le fichier sera écrit. Une implémentation vulnérable peut concaténer le nom fourni avec un répertoire de destination sans résoudre et contrôler le chemin final.
const destination = "/var/www/uploads/" + filename;
// Séquences typiques à tester :
../
..\\
%2e%2e%2f
Si ces séquences sont acceptées, un attaquant peut tenter de sortir du répertoire prévu et d’écrire dans un autre emplacement accessible au compte applicatif. Le même risque peut exister lorsque le nom lui-même est remplacé, mais que l’utilisateur contrôle un dossier, un identifiant de tenant ou un chemin d’import.
Le raisonnement est différent dans un stockage objet comme Amazon S3. Les clés ne sont pas des chemins de système de fichiers et ../ ne permet pas de « sortir » d’un bucket comme sur un disque local. Les problèmes pertinents concernent plutôt le contrôle insuffisant des clés et prefixes, les collisions, l’écrasement d’objets, l’isolation entre tenants ou la possibilité de placer un objet dans un namespace consommé par un autre composant.
Sur un système de fichiers, l’application doit construire ses chemins à partir d’identifiants générés côté serveur, canonicaliser la destination finale et vérifier qu’elle reste bien sous la racine autorisée. Dans un stockage objet, elle doit contrôler côté serveur le bucket, le prefix et la clé et les rattacher à l’identité authentifiée.
Modification de la configuration serveur via des fichiers uploadés
Certains serveurs web autorisent des fichiers de configuration par répertoire capables de modifier l’interprétation des objets présents dans une zone donnée. Si l’utilisateur choisit librement le nom de stockage et si l’upload aboutit dans un répertoire où ces mécanismes sont actifs, une extension apparemment inoffensive peut devenir exécutable après modification de la configuration.
Des exemples typiques sont .htaccess sur certaines configurations Apache ou web.config dans des environnements IIS. L’exploitabilité dépend entièrement du serveur et de ses règles, mais ces fichiers sont importants à tester lorsqu’une application repose essentiellement sur une blocklist d’extensions dynamiques.
Une architecture sûre empêche l’utilisateur de choisir le nom physique, place les uploads hors des répertoires exécutables et traite le contenu uploadé comme de la donnée, jamais comme de la configuration.
Contournements liés aux archives et extraction non sécurisée
Les archives nécessitent un traitement spécifique, car la validation du conteneur externe ne dit presque rien sur les objets qui apparaîtront après extraction. Un service peut identifier correctement un ZIP ou un TAR tout en restant vulnérable aux chemins malveillants, aux liens spéciaux, aux taux de compression extrêmes ou aux formats imbriqués.
Zip Slip et Path Traversal dans les archives
Zip Slip est une forme d’écriture arbitraire provoquée par une entrée d’archive dont le nom contient une traversée de répertoires ou un chemin absolu. Un extracteur vulnérable concatène la destination avec le nom de l’entrée et écrit le résultat sans vérifier le chemin canonique.
archive.zip
documents/report.txt
../../public/config.txt
# Une extraction non sécurisée peut écrire la seconde entrée hors du répertoire prévu.
Ce problème ne concerne pas uniquement ZIP. TAR, JAR, WAR, CPIO, RAR, 7z et d’autres formats peuvent transporter des informations de chemin. Pour chaque entrée, l’application doit résoudre la destination puis vérifier qu’elle reste sous la racine d’extraction avant toute écriture.
Liens symboliques et fichiers spéciaux
Certaines archives peuvent contenir des liens symboliques, hard links ou objets spéciaux du système de fichiers. Même si les séquences ../ sont refusées, un lien créé dans le répertoire d’extraction peut faire pointer une écriture ultérieure vers une zone située en dehors de la sandbox.
Si le produit n’a aucune raison fonctionnelle de prendre en charge ces objets, la meilleure politique consiste à les refuser. Dans le cas contraire, le moteur d’extraction doit exposer et appliquer explicitement une politique sûre de gestion des liens.
Decompression bombs et amplification des ressources
La taille compressée n’est pas une mesure fiable du coût réel d’un fichier. Une archive de quelques mégaoctets peut se déployer en dizaines de gigaoctets, contenir des millions de petites entrées, recourir à une profondeur d’imbrication importante ou déclencher des parsers très coûteux.
Un audit doit donc considérer la taille décompressée attendue, le taux de compression, le nombre d’entrées, la profondeur, la durée d’extraction et les ressources consommées. Le même raisonnement s’applique aux images aux dimensions extrêmes, aux contenus XML capables d’amplifier le traitement, aux vidéos nécessitant un transcodage lourd ou aux documents complexes.
Abus de HTTP PUT et WebDAV
Toutes les possibilités d’écriture ne passent pas par un formulaire HTML. Certains environnements autorisent l’écriture de fichiers via PUT, notamment lorsque WebDAV est activé ou qu’un serveur est mal configuré.
PUT /uploads/test.txt HTTP/1.1
Host: example.com
Content-Type: text/plain
Content-Length: 12
test content
Si le serveur accepte des requêtes PUT non authentifiées ou insuffisamment restreintes, un attaquant peut écrire directement dans un emplacement accessible sans traverser la logique applicative habituelle. Les contrôles d’extension, de MIME, d’antivirus ou de stockage implémentés dans l’application sont alors complètement contournés.
Ce type de faiblesse provient généralement d’une configuration serveur : WebDAV exposé, règle de reverse proxy permissive, endpoint de développement oublié ou droits d’écriture trop larges. Les méthodes HTTP inutiles doivent être désactivées et les répertoires publics ne doivent pas devenir des zones d’écriture arbitraire.
Race conditions lors de l’upload
Les race conditions ciblent l’intervalle entre l’écriture du fichier, sa validation, son traitement et sa suppression éventuelle. Certaines applications stockent temporairement le fichier avant la fin du scan ou de l’analyse. S’il est accessible pendant cette fenêtre, un attaquant peut tenter de l’utiliser avant qu’il ne soit placé en quarantaine ou supprimé.
1. Le fichier est uploadé.
2. Il est stocké temporairement.
3. La validation ou le scan antivirus débute.
4. Le fichier est accessible pendant une courte période.
5. L’application le supprime ou le bloque si le contrôle échoue.
La faiblesse ne réside pas dans la règle de validation, mais dans le moment auquel le contenu devient accessible. Le risque augmente lorsque le traitement est asynchrone et qu’une URL est retournée immédiatement alors que le scan ou la conversion s’effectue en arrière-plan.
Une conception sûre maintient les nouveaux fichiers dans une zone de quarantaine inaccessible aux utilisateurs et aux interpréteurs jusqu’à la réussite de tous les contrôles. Le même problème peut exister avec les imports depuis une URL : le système télécharge d’abord le contenu, crée des métadonnées ou une ressource temporaire puis termine seulement ensuite sa validation.
Exploiter les vulnérabilités d’upload de fichiers
Contourner un contrôle d’upload n’est que la première étape. L’impact réel dépend de ce que l’application, le serveur, le navigateur ou un composant tiers fera ensuite du fichier accepté.
Dans la plupart des scénarios graves, le fichier devient exploitable parce qu’un autre composant lui attribue une signification active. Le navigateur peut interpréter un document HTML ou SVG, le serveur web peut exécuter un script, un moteur de conversion peut parser un PDF, un parser XML peut résoudre des entités externes et un worker peut télécharger des ressources référencées par le contenu. Comprendre l’exploitation suppose donc de suivre le fichier au-delà de l’upload initial.
XSS stockée via des fichiers SVG et HTML
Les fichiers SVG ne sont pas de simples images raster. Ils reposent sur XML et peuvent contenir des fonctionnalités actives lorsqu’ils sont interprétés comme des documents. Autoriser des fichiers .svg ou .html peut donc conduire à une XSS stockée si le contenu contrôlé par l’utilisateur est ensuite rendu dans un contexte permettant l’exécution de scripts, en particulier lorsqu’il est servi depuis le même origin que l’application principale.
const allowedExtensions = ["jpg", "jpeg", "png", "gif", "svg"];
app.post("/upload", upload.single("file"), (req, res) => {
const extension = req.file.originalname.split(".").pop().toLowerCase();
if (!allowedExtensions.includes(extension)) {
return res.status(400).send("Invalid file type");
}
res.send("File uploaded successfully");
});
L’impact dépend toutefois du contexte exact de restitution. Les navigateurs modernes appliquent des restrictions importantes lorsqu’un SVG est chargé uniquement comme une image, par exemple via une balise <img> : dans ce contexte, l’exécution de scripts et le chargement de certaines ressources externes sont généralement bloqués. Ces restrictions ne s’appliquent pas de la même manière lorsque le SVG est ouvert comme document autonome ou intégré via <object>, <iframe> ou <embed>. Un SVG injecté inline dans un document HTML constitue encore un autre contexte.
Cette nuance est fondamentale pendant un pentest. Il ne faut pas conclure qu’un simple upload de SVG produit automatiquement une XSS. L’auditeur doit vérifier l’URL réelle, les en-têtes Content-Type et Content-Disposition, l’origin, le mode d’intégration et le contexte de navigation. Lorsque le SVG n’est pas indispensable au métier, convertir les images vers un format raster de confiance comme PNG ou JPEG réduit fortement la surface d’attaque. S’il est nécessaire, son contenu doit être nettoyé avec une bibliothèque spécialisée et servi dans un contexte restreint, idéalement séparé de l’origin principal.
Le même principe s’applique aux fichiers HTML, XHTML ou autres formats directement interprétables par le navigateur. Un stockage considéré comme « statique » ne suffit pas à rendre un contenu inoffensif si le navigateur le traite finalement comme un document actif avec les cookies, l’origin ou les privilèges de l’application.
Exécution de code à distance via l’upload de webshells
Une exécution de code à distance (RCE) peut survenir lorsqu’un fichier uploadé est stocké dans un emplacement où le serveur web ou une autre couche l’interprète comme du code.
Un handler PHP très permissif peut par exemple ressembler à ceci :
<?php
$uploadDir = __DIR__ . "/uploads/";
$targetPath = $uploadDir . basename($_FILES["file"]["name"]);
move_uploaded_file($_FILES["file"]["tmp_name"], $targetPath);
echo "File uploaded successfully";
?>
Cette implémentation cumule plusieurs faiblesses : elle conserve le nom fourni par le client, ne valide pas le type du fichier, écrit dans un répertoire accessible depuis le web et n’empêche pas le stockage de contenu exécutable. Si le serveur est configuré pour interpréter PHP dans ce répertoire, un fichier .php peut être exécuté lorsqu’un utilisateur accède ensuite à son URL.
Le même raisonnement vaut pour d’autres technologies. Selon la configuration, des extensions ASP.NET, JSP, PHTML ou d’autres formats dynamiques peuvent être interprétées. Le risque ne dépend donc pas uniquement de PHP, mais du lien entre le type de fichier accepté et les handlers actifs sur le chemin de stockage.
La chaîne d’exploitation repose sur deux conditions distinctes : l’attaquant doit parvenir à placer un fichier interprétable et un second événement doit provoquer son exécution. Casser l’une ou l’autre suffit à réduire considérablement le risque. Une allowlist stricte réduit la capacité de placer du code, tandis qu’un stockage non exécutable hors web root empêche le serveur d’interpréter le contenu même si la validation échoue.
Traitement malveillant de PDF et de documents
Les PDF, documents Office, images et médias sont souvent dangereux non pas parce que le serveur web les exécute directement, mais parce que l’application les parse ou les transforme automatiquement. La génération d’aperçus, l’OCR, l’extraction de métadonnées, la création de miniatures et les conversions de format augmentent la surface de confiance en exposant des bibliothèques complexes à des octets contrôlés par l’utilisateur.
const { exec } = require("child_process");
app.post("/upload-pdf", upload.single("file"), (req, res) => {
const inputPath = req.file.path;
const outputPath = `/tmp/previews/${req.file.originalname}.png`;
exec(`convert ${inputPath} ${outputPath}`, (error) => {
if (error) return res.status(500).send("Preview generation failed");
res.send("PDF uploaded and preview generated");
});
});
Cette construction introduit deux classes de risques. Le premier provient du parser lui-même : une vulnérabilité dans ImageMagick, Ghostscript, LibreOffice, ExifTool, FFmpeg ou l’un de leurs delegates peut être atteignable via un fichier spécialement conçu. Le second est indépendant du format : si un nom ou un chemin contrôlé par l’utilisateur est interpolé dans une commande shell, l’application peut devenir vulnérable à une injection de commandes.
Une API de lancement de processus prenant les arguments séparément est préférable à la construction d’une commande sous forme de chaîne.
const { spawn } = require("child_process");
spawn("convert", [inputPath, outputPath], { shell: false });
Cette modification réduit le risque d’injection shell, mais ne rend pas le moteur de conversion intrinsèquement sûr. Le composant doit rester isolé dans une sandbox, avec une identité non privilégiée, des limites strictes de CPU et de mémoire, un système de fichiers minimal, aucune information sensible inutile et des connexions réseau sortantes fortement restreintes.
Il faut également distinguer les risques côté serveur de ceux qui affectent la personne qui ouvre le document. Un PDF ou un document Office peut contenir des liens, objets embarqués, macros ou autres contenus actifs. Leur exécution dépend du format, du lecteur et de sa configuration, mais une plateforme d’upload publique peut devenir un canal de distribution de malware ou de phishing même si le serveur lui-même n’est jamais compromis.
Attaques XML via des fichiers uploadés
Les attaques XML apparaissent lorsque le contenu uploadé est traité par un parser dont la configuration autorise des fonctionnalités dangereuses.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(uploadedFile);
Selon le langage, la bibliothèque et sa version, la configuration par défaut peut permettre des déclarations de type de document ou la résolution d’entités externes. Un document contrôlé par l’attaquant peut alors pousser le parser à accéder à des ressources qui n’auraient jamais dû être exposées.
Une configuration plus sûre désactive explicitement les fonctionnalités qui ne sont pas nécessaires.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(uploadedFile);
Cet exemple montre pourquoi la validation d’upload ne suffit pas. Un fichier peut avoir une extension autorisée tout en exploitant le parser chargé de son traitement.
Le XML peut aussi être caché dans un format conteneur plutôt qu’être reçu directement sous l’extension .xml. Les formats Office modernes, SVG et de nombreux formats applicatifs utilisent XML en interne. L’audit doit donc identifier quels fichiers internes sont réellement parsés et avec quelle configuration.
SSRF via le traitement de fichiers
Une SSRF peut apparaître lorsqu’un fichier contient des références vers des ressources externes que le serveur charge automatiquement pendant le traitement.
Une application peut par exemple accepter un document HTML ou SVG et le convertir en image ou en PDF à l’aide d’un navigateur headless.
app.post("/render", upload.single("file"), async (req, res) => {
const html = fs.readFileSync(req.file.path, "utf8");
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, {
waitUntil: "networkidle0"
});
await page.pdf({ path: "/tmp/output.pdf" });
res.send("File rendered successfully");
});
Si le contenu contient une ressource distante, le renderer peut tenter de la récupérer depuis le réseau du serveur.
<img src="https://attacker-controlled.example/image.png">
Dans un environnement vulnérable, le même mécanisme peut atteindre des services internes ou des destinations link-local inaccessibles directement depuis Internet. La vulnérabilité n’est pas l’upload en lui-même, mais le fait qu’un contenu contrôlé par l’utilisateur influence une requête sortante effectuée par un composant de confiance.
La protection consiste à limiter les connexions réseau des workers, bloquer les plages privées et link-local lorsqu’elles ne sont pas nécessaires, désactiver le chargement de ressources externes et exécuter le renderer dans un environnement isolé.
Un scénario voisin concerne les fonctionnalités « upload depuis une URL », « importer une image », « générer un aperçu » ou certains convertisseurs. Dans ce cas, le client ne fournit pas directement les octets : il fournit une URL que le serveur va télécharger. Le composant doit alors être audité comme un fetcher potentiellement vulnérable à la SSRF, avec contrôle des redirections, des protocoles, des résolutions DNS successives et de l’accès aux destinations internes.
Déni de service via l’upload de fichiers
Une fonctionnalité d’upload peut être utilisée pour épuiser le stockage, la mémoire, le CPU, la bande passante ou la capacité des workers. Le risque ne se limite pas à envoyer un fichier gigantesque. Un attaquant peut transmettre un grand nombre de fichiers moyens, choisir des formats coûteux à parser, fournir des images aux dimensions extrêmes, créer des documents très imbriqués ou déclencher de nombreuses conversions.
from PIL import Image
from flask import Flask, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
@app.route("/upload", methods=["POST"])
def upload():
file = request.files["file"]
image = Image.open(file)
image.thumbnail((500, 500))
image.save("/tmp/resized.jpg")
return "Image processed"
La limite de cinq mégaoctets protège la taille de la requête, mais elle ne limite ni les dimensions de l’image ni la mémoire utilisée pendant le décodage. Elle ne contrôle pas non plus le nombre de requêtes, la concurrence des jobs, la taille des fichiers dérivés ou le coût d’un traitement ultérieur.
Une défense efficace impose des limites à plusieurs niveaux : taille par fichier et par requête, nombre de fichiers, dimensions, durée des médias, nombre d’entrées d’archive, taille décompressée, profondeur d’imbrication, durée de traitement, mémoire, CPU, espace disque temporaire et nombre de jobs concurrents. Des quotas par utilisateur ou tenant et des mécanismes de rate limiting évitent qu’un acteur unique monopolise les ressources.
Contrôles d’accès défaillants, divulgation et écrasement de fichiers
Une fonctionnalité peut être vulnérable même lorsque les octets acceptés sont totalement inoffensifs. Un contrôle d’autorisation défaillant peut permettre à un utilisateur de récupérer, remplacer, renommer ou supprimer le fichier d’un autre utilisateur. Dans une application multi-tenant, la même faiblesse peut exposer des documents appartenant à une autre organisation.
Les identifiants prédictibles, les URLs directes vers un stockage objet et les fichiers dérivés sont des sources fréquentes d’incohérences. Une application peut correctement protéger GET /documents/{id} tout en exposant publiquement /thumbnails/{id}.png, ou vérifier le propriétaire lors de la création sans refaire le contrôle lorsqu’un endpoint PUT remplace le fichier.
PUT /api/files/5f31c9d2 HTTP/1.1
Host: example.com
Cookie: session=attacker
Content-Type: application/pdf
[replacement file]
# Le serveur doit vérifier que l’utilisateur authentifié est autorisé
# à modifier cet objet précis, et pas seulement qu’il possède une session.
L’écrasement peut aussi résulter d’une collision de noms ou de clés de stockage partagées. Le problème devient particulièrement critique si l’objet remplacé est un template, un fichier de configuration, un asset statique, un package ou un document automatiquement consommé par un service de confiance.
Écriture arbitraire de fichiers via l’extraction d’archives
Lorsqu’une archive uploadée est extraite côté serveur, une entrée contenant un Path Traversal peut transformer une fonctionnalité d’import en primitive d’écriture arbitraire avec les privilèges du worker d’extraction.
L’impact dépend de ce que ce composant peut modifier. Écraser un fichier applicatif, une tâche planifiée, une configuration, une clé SSH, un plugin ou un asset servi par le web peut faire évoluer une simple faiblesse d’extraction vers une exécution de code ou une persistance.
L’audit doit donc vérifier la racine d’extraction, les contrôles de chemin canonique, les chemins absolus, les lettres de lecteur sur Windows, les liens symboliques, les doublons de noms et la politique d’écrasement. Le point dangereux n’est pas que l’archive ait passé la validation, mais l’opération d’écriture effectuée après son ouverture.
Formula Injection et contenus Office actifs
Les imports CSV et les feuilles de calcul créent un risque différent. Une valeur commençant par un préfixe de formule peut être interprétée comme une formule lorsque le fichier résultant est ouvert dans un tableur. Le comportement dépend du logiciel et de sa configuration, mais la frontière de sécurité est franchie lorsqu’un texte non fiable n’est plus traité comme une donnée mais comme une expression à évaluer.
name,department,comment
Alice,Finance,"=1+1"
# Pendant un test de sécurité, utiliser uniquement des formules non destructives
# afin de vérifier si les valeurs importées ou exportées sont évaluées.
Le risque peut apparaître à l’import, mais aussi à l’export. Une application peut stocker une valeur fournie par un utilisateur, la considérer comme une chaîne parfaitement normale puis l’exporter plus tard dans un CSV ouvert par un collaborateur. Il s’agit alors d’un scénario de second ordre : le contexte dangereux apparaît après le stockage.
Les fichiers Office avec macros ou objets embarqués introduisent une problématique voisine de distribution de contenu actif. Si ces fonctions ne sont pas nécessaires, les formats concernés peuvent être refusés ou reconstruits via CDR. S’ils sont indispensables, les documents doivent rester explicitement considérés comme non fiables et ne doivent pas être ouverts automatiquement par des processus privilégiés.
Abus du stockage objet et des URLs présignées
De nombreuses applications modernes envoient directement les fichiers depuis le navigateur ou l’application mobile vers un stockage objet. Le serveur applicatif autorise d’abord l’opération puis retourne un jeton temporaire ou une URL présignée. Le client transmet alors les octets au service de stockage sans les faire passer par le serveur web principal.
Cette architecture peut être sûre, mais elle déplace une partie essentielle de la sécurité vers la génération du jeton, le choix de la clé d’objet, la policy du bucket et la validation effectuée après l’upload.
PUT /tenant-a/uploads/2b7f... HTTP/1.1
Host: storage.example
Content-Type: application/pdf
[file content]
# La capacité d’upload doit être liée à un bucket et une clé choisis côté serveur,
# une durée de validité courte et l’opération strictement nécessaire.
Le pentester doit déterminer si l’utilisateur peut influencer le bucket, le prefix ou la clé, si l’URL peut écraser un objet existant, si elle peut être réutilisée, si les contraintes de contenu sont réellement appliquées et si l’objet devient public avant la validation côté serveur.
Avec Amazon S3, par exemple, un upload effectué via une URL présignée vers une clé qui existe déjà remplace l’objet correspondant. Des clés prédictibles ou choisies par le client peuvent donc créer un problème d’intégrité même lorsque la signature de l’URL est parfaitement valide.
Une URL présignée doit être considérée comme une capacité de type bearer. La journalisation, une durée de validité courte, des clés générées côté serveur, des permissions minimales et un workflow quarantaine-vers-publication sont plus importants que le fait de masquer l’URL elle-même.
Comment sécuriser une fonctionnalité d’upload de fichiers ?
Aucun contrôle isolé ne permet de sécuriser entièrement une fonctionnalité d’upload. Un attaquant peut cibler la validation du nom, le type du fichier, sa structure interne, le stockage, le traitement asynchrone, le composant de restitution ou les contrôles d’accès.
Une architecture robuste repose donc sur plusieurs couches indépendantes. Chaque mécanisme doit limiter une catégorie de risque et réduire l’impact d’une défaillance située ailleurs dans la chaîne.
N’autoriser que les types de fichiers nécessaires
La première question n’est pas « quelles extensions dangereuses faut-il bloquer ? », mais « quels formats sont réellement nécessaires ? ». Une allowlist réduite diminue mécaniquement le nombre de parsers, de contextes de rendu et de fonctionnalités actives que l’application doit supporter de manière sûre.
Si une photo de profil peut être représentée en JPEG ou PNG, autoriser SVG, PDF, HTML et des archives arbitraires ajoute des risques sans bénéfice fonctionnel. La politique doit donc être définie à partir du besoin métier et non à partir d’une liste de formats connus comme malveillants.
const allowedExtensions = ["jpg", "jpeg", "png", "pdf"];
const extension = getExtension(filename).toLowerCase();
if (!allowedExtensions.includes(extension)) {
rejectUpload();
}
Le contrôle doit intervenir après décodage et canonicalisation du nom et s’accompagner de limites explicites sur la longueur et les caractères autorisés. Une denylist peut rester utile comme garde-fou supplémentaire, mais elle ne doit jamais constituer la frontière de sécurité principale.
Valider les types MIME, les signatures et la structure des fichiers
Le Content-Type envoyé par le client est une métadonnée contrôlable et ne prouve rien sur le contenu réel. Les signatures binaires offrent un signal plus fiable, mais vérifier seulement les premiers octets ne valide pas toute la structure.
Une chaîne solide compare l’extension attendue, le type MIME déclaré, la signature détectée et, pour les formats à risque, le résultat d’un parser spécialisé.
import { fileTypeFromFile } from "file-type";
const type = await fileTypeFromFile(uploadedFile);
if (!["image/jpeg", "image/png"].includes(type.mime)) {
rejectUpload();
}
Pour les images, décoder puis réencoder vers un nouveau fichier permet de supprimer de nombreuses ambiguïtés, des données ajoutées en fin de contenu et certaines structures non nécessaires. Pour les documents, une solution de CDR ou une bibliothèque spécialisée peut être plus adaptée.
La validation doit échouer de manière sûre lorsque le format est ambigu, malformé ou non supporté. Le fichier doit rester en quarantaine tant que tous les contrôles obligatoires ne sont pas terminés.
Authentifier et autoriser l’upload, le téléchargement et la modification
Seuls les utilisateurs ayant réellement besoin de la fonctionnalité doivent pouvoir l’utiliser. Surtout, l’autorisation ne s’arrête pas après l’upload. Chaque lecture, remplacement, renommage, partage et suppression doit vérifier le droit sur l’objet précis.
Les miniatures, aperçus et versions converties doivent hériter du même modèle de protection. Une copie dérivée publique peut contourner à elle seule une autorisation correcte sur l’original.
Dans un environnement multi-tenant, l’identifiant de tenant utilisé dans la base, les chemins ou les clés d’objet doit provenir du contexte authentifié côté serveur et non d’un paramètre fourni par le client. Un fichier public doit l’être parce que le produit l’a explicitement décidé, pas parce qu’un bucket ou une URL est trop permissif.
Générer les noms de fichiers et les clés de stockage côté serveur
Le nom fourni par l’utilisateur ne doit pas devenir le nom physique utilisé pour le stockage. L’application doit générer un identifiant imprévisible, par exemple un UUID, et conserver le nom d’origine uniquement comme métadonnée d’affichage.
import crypto from "crypto";
const storageId = crypto.randomUUID();
const storedFilename = `${storageId}.pdf`;
// Le nom d’origine n’est conservé que comme métadonnée validée.
const originalFilename = sanitiseDisplayName(req.file.originalname);
Cette stratégie réduit les collisions, les possibilités de manipulation de chemins et les noms ayant une signification spéciale pour le système d’exploitation ou le serveur web. Si l’extension doit être conservée pour des raisons opérationnelles, elle doit être dérivée du type validé côté serveur et non copiée aveuglément depuis la requête.
La même règle vaut pour les clés de stockage objet : elles doivent être construites à partir d’un prefix de tenant contrôlé par le serveur et d’un identifiant généré, pas directement à partir d’un nom ou chemin choisi par le client.
Stocker les fichiers hors des emplacements web exécutables
Un défaut architectural classique consiste à écrire directement les uploads dans un répertoire accessible et interprétable par le serveur web, par exemple /var/www/html/uploads/. Une seule erreur de validation peut alors se transformer en exécution de code.
Risqué : /var/www/html/uploads/document.pdf
Plus sûr : /var/app/storage/uploads/7c8f9a4e
Restitution : GET /download/7c8f9a4e
Il est préférable d’utiliser un stockage distinct ou un répertoire hors web root. Le contenu peut ensuite être servi par un endpoint applicatif contrôlé ou par un mécanisme CDN/storage correctement configuré, qui applique authentification, autorisation, rate limiting, en-têtes HTTP sûrs et journalisation.
Si une diffusion publique est nécessaire, un origin séparé incapable d’exécuter du code serveur et doté de permissions limitées réduit le risque qu’un contenu utilisateur soit traité comme une ressource applicative de confiance.
Appliquer le principe du moindre privilège
Il n’existe pas de valeur universelle de chmod rendant un répertoire d’upload sûr. Les permissions correctes dépendent des opérations nécessaires à chaque composant. L’objectif consiste à donner à chaque service uniquement les droits dont il a besoin et à supprimer toute capacité d’exécution des contenus utilisateurs lorsque celle-ci n’est pas explicitement requise.
Un service d’ingestion peut avoir besoin d’écrire dans une quarantaine. Un worker antivirus peut n’avoir besoin que d’un accès en lecture à cette zone et d’un droit d’écriture vers un répertoire de sortie. Le composant qui sert les fichiers publiés peut être limité à la lecture seule.
Le serveur web principal ne doit pas hériter automatiquement d’un accès en écriture à toutes les zones de traitement ou de publication. Le même principe doit s’appliquer aux rôles IAM cloud, aux policies de buckets et aux credentials utilisés par les workers.
Imposer des limites de taille, de nombre, de quota et de traitement
Une limite de taille de requête est nécessaire, mais elle ne couvre qu’une dimension du risque. Il faut également limiter le nombre de fichiers par requête, le quota de stockage par utilisateur ou tenant, la fréquence des uploads, le nombre de jobs concurrents, les dimensions d’images, la durée des médias, le nombre d’entrées d’archive, le taux d’expansion, la profondeur d’imbrication et le temps maximal de traitement.
Ces contraintes doivent être appliquées le plus tôt possible, avant les opérations coûteuses. Les workers doivent aussi disposer de plafonds explicites de CPU, mémoire, disque temporaire et durée d’exécution. Les répertoires temporaires nécessitent des quotas et un nettoyage fiable, notamment après les erreurs.
Lorsqu’un traitement décompresse ou transforme le fichier, la taille de sortie doit elle aussi être contrôlée. Une requête de cinq mégaoctets peut provoquer plusieurs gigaoctets de données après décompression ou conversion.
Sécuriser l’extraction des archives
L’extraction doit s’effectuer dans un répertoire temporaire isolé, non exécutable et dépourvu de données sensibles. Pour chaque entrée, l’application doit résoudre le chemin final puis vérifier qu’il se trouve bien sous la racine autorisée avant toute écriture.
Les chemins absolus, traversées, fichiers spéciaux et liens symboliques ou hard links doivent être rejetés sauf besoin métier explicite accompagné d’une implémentation sûre. Les limites sur le nombre d’entrées, la taille totale décompressée, la taille individuelle, le taux de compression, la profondeur et la durée d’extraction doivent être indépendantes des limites appliquées à la requête HTTP.
Il faut aussi définir clairement le comportement en cas de doublon de nom. Une extraction qui écrase silencieusement un fichier existant peut créer une primitive d’intégrité exploitable. Enfin, chaque fichier extrait doit subir les mêmes contrôles de type, contenu et malware qu’un fichier uploadé directement avant d’être publié ou consommé par un autre système.
Servir les fichiers uploadés de manière sûre
La restitution est elle-même un contrôle de sécurité. Le serveur doit renvoyer un Content-Type déterminé à partir d’une validation de confiance et non simplement recopier le MIME fourni par le client.
X-Content-Type-Options: nosniff permet de demander au navigateur de respecter le type déclaré au lieu d’essayer de déduire un autre format. Lorsque le contenu n’a pas besoin d’être rendu inline, Content-Disposition: attachment réduit la probabilité qu’un fichier actif soit interprété comme une page dans le navigateur.
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
X-Content-Type-Options: nosniff
Cache-Control: private, no-store
Pour les contenus utilisateurs fortement non fiables, un origin séparé accompagné d’une politique de sécurité restrictive peut réduire l’impact des comportements same-origin si un fichier HTML, SVG ou similaire est accidentellement interprété. L’authentification, le cache et le contrôle d’accès doivent néanmoins rester correctement conçus afin que cet origin séparé ne devienne pas une source de fuite de documents privés.
Sécuriser le stockage objet et les workflows utilisant des URLs présignées
Dans un upload direct vers le cloud, l’application doit générer côté serveur le bucket et la clé de destination et les lier à l’utilisateur ou au tenant authentifié. Le principal qui génère la capacité présignée doit disposer des permissions minimales et la durée de validité doit rester courte.
Il faut éviter les permissions génériques permettant au client de choisir librement une clé, un prefix ou une opération. Une capacité d’écriture sur un namespace trop large transforme une fonctionnalité limitée en primitive de modification d’objets plus étendue que prévu.
L’objet nouvellement envoyé doit être considéré comme en quarantaine. Une callback ou un appel applicatif de finalisation peut vérifier qu’il existe, contrôler sa taille et ses métadonnées, lancer la validation serveur et le scan puis seulement marquer l’objet comme publié ou le déplacer/copier vers une zone de diffusion.
Les ACL ou bucket policies ne doivent pas exposer publiquement le fichier avant cet état. Lorsque l’écrasement serait dangereux, des clés uniques doivent être générées et des mécanismes comme le versioning ou les opérations conditionnelles peuvent compléter la protection.
Il est utile de journaliser à la fois la création de l’URL présignée et l’événement de stockage correspondant afin de rattacher un upload suspect à l’identité applicative qui l’a autorisé.
Protéger les endpoints d’upload contre la CSRF
Lorsqu’une application authentifie les actions sensibles avec des credentials automatiquement envoyés par le navigateur, par exemple des cookies de session, l’upload doit bénéficier des mêmes protections CSRF que les autres opérations modifiant l’état.
Selon l’architecture, cela peut passer par des tokens synchronisés, des attributs SameSite appropriés, la validation de Origin ou Referer et l’interdiction de schémas cross-origin non nécessaires.
La protection CSRF ne remplace évidemment pas la validation du fichier. Les APIs authentifiées par bearer token explicite présentent par ailleurs un modèle de menace différent. Le principe est simplement qu’un upload, un remplacement ou une suppression est une action d’état et qu’un attaquant ne doit pas pouvoir forcer le navigateur d’un utilisateur connecté à soumettre un fichier sans action intentionnelle.
Isoler les traitements de fichiers dans une sandbox
Les composants qui parsèrent des fichiers doivent être isolés de l’application principale parce qu’ils exécutent des bibliothèques complexes sur des octets contrôlés par un attaquant. ImageMagick, Ghostscript, ExifTool, LibreOffice, FFmpeg, les moteurs OCR et les bibliothèques PDF sont des exemples courants.
Une architecture plus sûre place les nouveaux fichiers dans une zone de quarantaine puis délègue le parsing ou la conversion à un worker dédié. Celui-ci reçoit uniquement l’objet qu’il doit traiter, s’exécute sous une identité non privilégiée, dispose de limites strictes de CPU et mémoire, n’embarque pas de secrets inutiles et ne peut accéder qu’à un système de fichiers minimal.
Les connexions réseau sortantes doivent être restreintes, car un parser ou renderer capable d’accéder librement au réseau peut transformer un contenu actif en SSRF ou en canal d’exfiltration. Une fois le traitement terminé, seul le résultat validé ou reconstruit est promu vers le stockage permanent.
Des conteneurs, workers dédiés, machines virtuelles ou fonctions serverless peuvent fournir cette isolation selon l’environnement. La sandbox ne remplace toutefois pas le patch management : elle réduit le blast radius d’une vulnérabilité, mais le composant continue à manipuler des contenus non fiables et doit rester à jour.
Content Disarm and Reconstruction (CDR)
Le Content Disarm and Reconstruction adopte une philosophie différente de l’antivirus. Au lieu d’essayer de décider si chaque objet contenu dans un document est malveillant, le moteur parse le fichier, supprime les fonctionnalités non autorisées par la politique de sécurité puis reconstruit une nouvelle représentation ne contenant que les éléments nécessaires.
Pour un PDF, cela peut consister à retirer les scripts, actions de lancement, pièces jointes embarquées ou autres objets actifs avant de reconstruire le document. Pour un fichier Office, le processus peut supprimer macros, exécutables embarqués et objets non supportés tout en préservant le texte, les images et la mise en forme utiles au métier.
La politique exacte dépend du format et du niveau de fidélité attendu. Le CDR est particulièrement pertinent lorsque l’organisation reçoit régulièrement des documents de tiers et souhaite une assurance plus forte qu’un simple contrôle de signature ou un scan antivirus.
Le moteur CDR doit lui-même être isolé, puisqu’il parse du contenu non fiable. La reconstruction réduit la surface d’attaque du fichier final mais ne rend pas le moteur de traitement exempt de vulnérabilités.
Journalisation, supervision et alerting
La sécurité ne s’arrête pas une fois le fichier accepté. Les logs doivent permettre de reconstruire qui a autorisé l’upload, quels contrôles ont été effectués, quel identifiant de stockage a été généré, quels traitements ont eu lieu, qui a ensuite accédé au contenu et pourquoi un fichier a été rejeté ou placé en quarantaine.
{
"user_id": "12345",
"file_id": "7c8f9a4e",
"original_filename": "invoice.php.jpg",
"detected_mime": "image/jpeg",
"upload_result": "rejected",
"reason": "extension_policy"
}
Parmi les signaux intéressants figurent les échecs répétés de validation, les tentatives d’upload d’extensions exécutables ou de configuration, les incohérences inhabituelles entre MIME et signature, les volumes anormaux, l’épuisement de quotas, l’augmentation des erreurs de traitement, les détections antivirus, l’expansion inattendue d’archives, les tentatives d’accès inter-tenant et l’apparition de formats atypiques pour une fonctionnalité donnée.
Les alertes doivent être adaptées au contexte pour éviter que l’activité métier normale masque les comportements réellement suspects. La rétention des événements doit également permettre l’investigation : si un fichier malveillant est découvert plusieurs jours plus tard, l’équipe doit pouvoir retrouver le compte d’origine, les objets concernés, les fichiers dérivés, les workers qui ont manipulé le contenu et les utilisateurs qui l’ont consulté ou téléchargé.
Conclusion
Sécuriser un upload de fichiers est difficile précisément parce qu’un fichier n’est jamais « validé une fois pour toutes ». Le même objet peut recevoir un nom dans le navigateur, être inspecté par le code applicatif, analysé par plusieurs bibliothèques, transformé par des workers, stocké sur un système de fichiers ou dans un bucket, mis en cache par un CDN puis finalement affiché ou ouvert par un autre utilisateur.
À chacune de ces transitions, la manière dont les octets sont interprétés peut changer. Une donnée considérée comme une image par un premier contrôle peut devenir un document actif pour un navigateur. Un PDF accepté par l’application peut devenir une entrée hostile pour un moteur de conversion. Une archive correctement identifiée peut produire un chemin arbitraire après extraction. Une clé de stockage jugée sans danger peut permettre l’écrasement d’un objet appartenant à un autre workflow.
C’est pourquoi le modèle de sécurité le plus robuste repose sur la défense en profondeur. Il faut limiter les formats aux besoins réels, normaliser les noms, valider le contenu côté serveur, générer les identifiants de stockage, isoler les fichiers non fiables, appliquer des autorisations au niveau de chaque objet, maintenir le contenu inaccessible jusqu’à la fin des contrôles, limiter les privilèges et les ressources des parsers et maîtriser la manière dont le fichier final est restitué.
Aucun contrôle isolé ne fournit une couverture équivalente. Une extension autorisée ne prouve pas le format réel. Un MIME correct peut être falsifié. Des magic bytes valides ne garantissent pas la sécurité de toute la structure. Un antivirus peut manquer une charge inconnue. Un fichier correctement validé peut encore devenir dangereux s’il est servi avec de mauvais en-têtes ou consommé par un composant vulnérable.
Pour un pentester, cette même logique devient une méthodologie. L’objectif est de cartographier tout le cycle de vie, de provoquer des désaccords contrôlés entre les différentes couches de validation, de suivre le fichier vers tous les consommateurs secondaires et de tester les contrôles d’accès avec la même attention que le contenu lui-même.
Les vulnérabilités d’upload les plus critiques n’apparaissent donc pas nécessairement au moment du POST. Elles surgissent souvent plus tard, lorsqu’un autre composant fait confiance à ce que la chaîne d’upload a laissé passer. Comprendre ce décalage entre validation, stockage, traitement et utilisation est la clé pour auditer et sécuriser durablement ces fonctionnalités.
Références
Les ressources suivantes complètent les principes, techniques de test et mesures de sécurité présentés dans ce guide :
OWASP File Upload Cheat Sheet : https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
OWASP Input Validation Cheat Sheet : https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
PortSwigger Web Security Academy – File upload vulnerabilities : https://portswigger.net/web-security/file-upload
Amazon S3 – Download and upload objects with presigned URLs : https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
MDN – SVG as an image : https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_as_an_image
MDN – X-Content-Type-Options : https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options