Aller au contenu

Attaque MIME sniffing par fichier téléversé

Le MIME sniffing transforme un service de téléversement mal isolé en point d’exécution de code dans le navigateur. Le défaut paraît mineur, souvent rangé avec les détails de proxy ou les caprices de navigateur, jusqu’au moment où un faux JPEG devient une page active sous un domaine qui porte encore des cookies de session.

→ Lire l’explication complète de la marmotte

Le type MIME (Multipurpose Internet Mail Extensions) indique la nature d’une ressource HTTP (Hypertext Transfer Protocol), par exemple image/jpeg, text/plain ou text/html. Le serveur le transmet au navigateur dans l’en-tête Content-Type. Une plateforme de téléversement qui décide que tout fichier finissant par .jpg est une image répondra généralement Content-Type: image/jpeg.

Historiquement, certains navigateurs ont choisi de regarder les premiers octets du contenu lorsque le type déclaré semblait douteux, absent ou incompatible avec le contexte d’usage. Ce comportement de détection, nommé MIME sniffing, visait la compatibilité avec des serveurs qui étiquetaient tout en text/plain ou application/octet-stream. La compatibilité a un coût : le navigateur devient arbitre du contenu à la place du serveur. Mauvaise idée dès qu’un attaquant contrôle ce contenu.

Une charge peut donc être déposée sous un nom tel que vacances.jpg alors que les octets commencent par du balisage HTML (HyperText Markup Language) :

<!-- vacances.jpg : son extension ment, son contenu est du HTML actif -->
<!doctype html>
<script>
document.body.textContent = 'Exécution démontrée';
</script>

Dans un navigateur historique ou un moteur configuré pour effectuer cette détection permissive, une navigation directe vers l’URL du fichier peut interpréter cette réponse comme un document HTML malgré Content-Type: image/jpeg. Le script s’exécute alors dans l’origine du site qui sert le fichier. Cette origine est le triplet schéma, nom d’hôte et port utilisé par le navigateur pour séparer les applications web.

Le compromis central est net : tolérer les erreurs de typage des serveurs améliore l’accès à du contenu ancien et mal configuré, mais retire au serveur l’autorité sur le contrat de données. Pour les fichiers contrôlés par des utilisateurs, ce compromis doit être refusé. La recommandation est de traiter tout stockage de téléversement comme hostile, même quand il ne contient officiellement que des avatars de chatons ou des tableurs qui ont juré de ne rien faire.

Les navigateurs actuels ont réduit plusieurs variantes de cette attaque, notamment dans les destinations script et style, lorsqu’ils reçoivent X-Content-Type-Options: nosniff. Cela ne rend pas l’erreur acceptable. Les règles diffèrent selon le navigateur, la version, le type déclaré, le contexte d’inclusion et l’historique de compatibilité. Une défense qui dépend de la liste des heuristiques d’un moteur est une défense dont la date de péremption a déjà commencé à courir.

Laboratoire de téléversement sans protection de type

Section intitulée « Laboratoire de téléversement sans protection de type »

La démonstration exige un environnement isolé et un navigateur historique affecté par l’interprétation permissive visée. Les navigateurs maintenus ne doivent pas être dégradés pour reproduire un défaut ancien : une machine virtuelle sans accès aux comptes, aux jetons ni au réseau interne convient. L’objectif est d’observer le mécanisme, pas de collectionner les mauvaises décisions de 2009.

Pré-requis :

  • un serveur Nginx 1.24 ou ultérieur, installé dans un conteneur ou une machine de laboratoire ;
  • un répertoire inscriptible exclusivement par le processus de téléversement ;
  • un navigateur ou une machine virtuelle explicitement documenté comme affecté par le comportement testé ;
  • aucun cookie de production, aucun fournisseur d’identité, aucun accès à des données réelles ;
  • une capture HTTP avec les outils de diagnostic décrits dans les pratiques réseau.

La configuration suivante sert tout fichier du répertoire uploads avec un type forcé et sans X-Content-Type-Options. Elle reproduit volontairement une erreur, donc pas de recyclage discret dans une configuration réelle sous prétexte que « le test marche ».

# nginx.conf : laboratoire délibérément vulnérable
events {}
http {
server {
listen 8080;
server_name localhost;
location /uploads/ {
alias /srv/uploads/;
# Défaut voulu : le serveur affirme que tout est une image JPEG.
default_type image/jpeg;
# Défaut voulu : absence de X-Content-Type-Options: nosniff.
# Un navigateur permissif peut inspecter le corps reçu.
add_header Cache-Control "no-store";
}
}
}

Créer ensuite le fichier de test :

Fenêtre de terminal
# Le suffixe est .jpg, mais le contenu est un document HTML.
mkdir -p /srv/uploads
cat > /srv/uploads/preuve.jpg <<'EOF'
<!doctype html>
<meta charset="utf-8">
<title>Preuve MIME sniffing</title>
<script>
document.body.innerHTML = '<h1>Script exécuté</h1>';
</script>
EOF
# Vérifier le contrat HTTP erroné.
curl -i http://localhost:8080/uploads/preuve.jpg

La réponse doit annoncer Content-Type: image/jpeg tout en livrant du balisage HTML. Dans un navigateur moderne, la charge peut être téléchargée, affichée comme texte, rendue comme image cassée ou bloquée selon le chemin d’accès et les protections applicables. Ce résultat est attendu. La démonstration d’exécution concerne précisément un moteur historique qui renonce à la déclaration serveur et reconnaît le contenu comme du HTML lors d’une navigation.

Il ne faut pas confondre cette variante avec l’inclusion <script src="/uploads/preuve.jpg">. Les navigateurs modernes appliquent des contrôles plus stricts aux scripts que lors de certaines navigations historiques. Une preuve qui ne s’exécute plus dans Chrome, Firefox ou Edge maintenus ne valide pas le serveur : elle indique seulement que le client actuel a absorbé une partie du choc. Le serveur reste incorrect et d’autres clients, proxys, aperçus de documents ou chaînes de traitement peuvent réintroduire l’ambiguïté.

L’exécution de script n’est intéressante que parce que l’origine du fichier porte une autorité. Si https://app.example héberge à la fois l’interface authentifiée et les fichiers utilisateurs sous https://app.example/uploads/, un document actif chargé depuis cette dernière URL appartient à app.example. Il peut lire les réponses accessibles à cette origine, appeler des interfaces applicatives avec les cookies attachés et modifier l’interface affichée à la victime.

Le drapeau HttpOnly empêche JavaScript de lire directement un cookie, mais il n’empêche pas le navigateur d’envoyer ce cookie avec une requête de même origine. Ce drapeau limite le vol brut de session, pas le détournement de session par actions authentifiées. SameSite réduit certains envois intersites, pas le script déjà exécuté sous le site lui-même. Croire l’inverse revient à installer un extincteur dans la cuisine puis déclarer l’immeuble ignifugé.

Les variantes fréquentes sont les suivantes :

  • un fichier étiqueté image mais interprété comme HTML lors d’une navigation ;
  • un fichier texte contenant du HTML actif et servi sans type fiable ;
  • un document SVG (Scalable Vector Graphics), qui est du XML (Extensible Markup Language) et peut contenir des scripts ou des références externes selon le contexte ;
  • un fichier HTML téléversé, servi avec un type correct mais sous le domaine applicatif ;
  • une transformation d’image qui conserve des métadonnées, un type inattendu ou un contenu original accessible par une autre URL.

La validation sur l’extension seule ne protège rien. La validation sur l’en-tête Content-Type envoyé par le client ne protège guère plus : cet en-tête est une déclaration non authentifiée. La détection par signature binaire, parfois appelée magic bytes, apporte une preuve partielle du format. Un JPEG commence normalement par FF D8 FF, un PNG (Portable Network Graphics) par 89 50 4E 47 0D 0A 1A 0A. Cette vérification doit être complétée par un décodeur réel qui lit le fichier, rejette les structures invalides et réencode l’image dans un format autorisé.

X-Content-Type-Options, validation et domaine isolé

Section intitulée « X-Content-Type-Options, validation et domaine isolé »

X-Content-Type-Options: nosniff interdit au navigateur de deviner un autre type dans les contextes où cette directive est appliquée. Cet en-tête doit être ajouté sur toutes les réponses, et particulièrement sur les contenus statiques, les erreurs HTTP et les objets téléversés. Une règle placée seulement sur la page d’accueil est décorative, donc parfaitement compatible avec une slide de gouvernance.

La configuration corrigée doit déclarer le type à partir d’une liste blanche calculée côté serveur, jamais à partir du nom soumis. Un service d’images devrait accepter JPEG, PNG et WebP (Web Picture format), détecter leur format avec une bibliothèque maintenue, décoder puis réencoder. Le fichier original doit être supprimé ou stocké hors de toute route HTTP si une conservation métier l’impose.

# Réponse de diffusion durcie pour les objets déjà validés.
location /uploads/ {
alias /srv/public-images/;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "sandbox" always;
add_header Content-Disposition "attachment" always;
add_header Cache-Control "public, max-age=31536000, immutable";
}

Content-Disposition: attachment demande le téléchargement plutôt que l’affichage. Ce choix casse la prévisualisation directe, mais réduit l’exposition lorsque le type ne peut pas être garanti. Pour une galerie d’images validées et réencodées, l’affichage en ligne reste acceptable avec nosniff. La question qui tranche est fonctionnelle : faut-il présenter un objet utilisateur dans le navigateur, ou seulement le remettre comme pièce jointe ?

La barrière structurante reste l’isolation d’origine : servir les téléversements depuis https://usercontent.example-cdn.net, jamais depuis https://app.example. Le domaine de fichiers ne doit recevoir aucun cookie de session de l’application, ne doit pas partager Domain=.example avec elle, et ne doit pas héberger d’API authentifiée. Un sous-domaine est parfois suffisant, mais un domaine réellement distinct évite les cookies parent trop larges et les erreurs de configuration. Les fondations de cette séparation recoupent les choix d’architecture cyber.

Une politique CSP (Content Security Policy) avec sandbox fournit une défense supplémentaire pour les documents éventuellement rendus. Elle ne remplace ni la validation ni le domaine séparé. Une CSP peut être absente sur une réponse d’erreur, remplacée par un proxy ou neutralisée par une directive trop permissive. Les couches sont nécessaires parce que les routes de téléchargement finissent toujours par accumuler des exceptions, puis des exceptions aux exceptions, puis une réunion pour expliquer pourquoi le fichier s’appelle final_final_2.jpg.

Le stockage de téléversement doit être traité comme une zone de quarantaine. Le processus de réception écrit dans un emplacement non publiable, calcule une empreinte cryptographique, analyse le format, applique éventuellement un antivirus, réencode, puis publie uniquement le dérivé validé. L’objet initial est soit détruit, soit conservé chiffré dans un compartiment sans diffusion HTTP directe.

Les clés de chiffrement au repos exigent une rotation documentée. Une DEK (Data Encryption Key) chiffre l’objet, tandis qu’une KEK (Key Encryption Key) chiffre ou protège la DEK. La rotation de KEK ne force pas nécessairement le rechiffrement de tous les objets si les DEK sont enveloppées ; la rotation de DEK l’exige. Cette distinction détermine la durée réelle d’une opération de reprise, pas le nom du produit de stockage.

La supervision doit alerter sur les refus de type, les échecs de décodage, les fichiers dont le contenu ne correspond pas au type annoncé, les accès directs aux originaux et les réponses sans X-Content-Type-Options. Les journaux doivent conserver l’identifiant d’objet, l’empreinte, le type détecté, le type servi et la décision de transformation. Sans ces quatre éléments, une investigation confondra vite un défaut applicatif avec un artefact de cache.

La sauvegarde doit inclure les objets validés, les métadonnées de type et les règles de publication. Restaurer uniquement les octets sans la base qui marque leur état de validation revient à remettre en ligne une quarantaine dont les étiquettes ont brûlé. Les principes de gestion de l’accès et des sessions restent détaillés dans la section identités, car l’isolation de domaine ne compense pas une session attachée à un cookie trop large.

Sans typage strict ni isolation, la porte de la ruche reste ouverte à tout ce que déposent les visiteurs. Une réponse mal étiquetée mise en cache par un CDN ou un proxy continue d’être distribuée telle quelle après correction du serveur, avec une durée de vie qui peut atteindre celle configurée pour les objets immuables. Les liens publics restent partageables et rejouables : chaque ouverture ultérieure rejoue la même ambiguïté, y compris hors navigateur, dans les aperçus de messagerie, les visionneuses intégrées et les robots d’indexation qui appliquent leurs propres heuristiques. Faute de réencodage, rien ne permet d’établir a posteriori qu’un objet déposé était inerte au moment de sa réception, ce qui laisse un doute sur l’ensemble du stock lors d’une reprise. Le domaine applicatif peut aussi se retrouver signalé par des filtres de contenu tiers, avec un blocage qui touche les pages légitimes servies par la même origine.

Un serveur web envoie deux choses quand un fichier est demandé : le contenu du fichier et une indication sur sa nature. Cette indication peut dire que le fichier est une image, un texte ou une page web. Certains anciens navigateurs ne faisaient pas toujours confiance à cette indication. Ils regardaient aussi le début du fichier pour décider eux-mêmes de ce qu’ils allaient afficher.

Une attaque devient possible lorsqu’un site accepte des fichiers envoyés par des utilisateurs et les publie sous la même adresse que son application. Une personne malveillante envoie alors un fichier nommé avec une extension d’image, mais contenant une page web avec du code actif. Le serveur croit reconnaître une image parce qu’il se fie au nom du fichier. Un navigateur ancien peut reconnaître à son tour une page web en lisant le contenu. Si la victime ouvre l’adresse du fichier, le code peut agir avec les droits accordés au site dans son navigateur.

La protection commence par empêcher le navigateur de changer le type annoncé par le serveur. Le serveur doit aussi vérifier le vrai format du fichier, le relire avec un programme capable de comprendre ce format, puis produire une nouvelle image propre. Enfin, les fichiers envoyés doivent être servis depuis un domaine séparé, sans les cookies de connexion de l’application. Si une vérification échoue, le fichier reste bloqué et ne doit jamais être publié.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.