Comment corriger une attaque path traversal
Section intitulée « Comment corriger une attaque path traversal »Une fonction de téléchargement qui reçoit un nom de fichier n’est pas une fonction de téléchargement, c’est une frontière de sécurité. Concaténer ce nom avec un répertoire applicatif sans contrôler le résultat permet de lire, et parfois d’écrire, hors du périmètre prévu.
Un service qui distribue des documents ressemble à un guichet où l’on demande un dossier par son nom. Si le guichet accepte n’importe quelle indication écrite sur le formulaire, quelqu’un peut tenter d’obtenir des papiers rangés dans une autre salle, y compris ceux qui n’auraient jamais dû quitter l’armoire.
→ Lire l’explication complète de la marmotte
Démonstration d’une lecture path traversal
Section intitulée « Démonstration d’une lecture path traversal »Le scénario repose sur une application Python minimale exécutée sous Linux. Elle doit servir uniquement dans un environnement de test isolé : conteneur jetable ou machine locale, jamais sur une machine contenant des secrets exploitables. Le fichier visé sera /etc/hostname, peu sensible mais suffisant pour prouver que la frontière de téléchargement est fictive.
Prérequis :
- Python 3.11 ou supérieur ;
- Flask 3.x ;
- un répertoire
/srv/downloadslisible par le processus ; - un compte de service non privilégié, même pour cette démonstration.
Créer le répertoire et l’application :
# Préparer un fichier normalement téléchargeable.sudo mkdir -p /srv/downloadsecho "contenu public" | sudo tee /srv/downloads/manuel.txt >/dev/nullsudo chmod -R a+rX /srv/downloads
# Installer Flask dans un environnement Python isolé.python3 -m venv .venv. .venv/bin/activatepip install "Flask>=3,<4"
# Écrire l'application volontairement vulnérable.cat > app.py <<'PY'from flask import Flask, abort, request, send_filefrom pathlib import Path
app = Flask(__name__)DOWNLOAD_ROOT = "/srv/downloads"
@app.get("/download")def download(): # Vulnérable : la valeur contrôlée par le client est concaténée telle quelle. name = request.args.get("name", "") path = DOWNLOAD_ROOT + "/" + name
if not Path(path).is_file(): abort(404)
return send_file(path, as_attachment=True)
if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=False)PY
python app.pyDans un second terminal, la requête normale fonctionne :
# Lecture prévue : le fichier est sous /srv/downloads.curl -s "http://127.0.0.1:5000/download?name=manuel.txt"Puis la remontée de répertoires sort de la zone autorisée :
# %2e%2e est la forme encodée de "..".# Flask décode le paramètre avant que l'application construise le chemin.curl -s "http://127.0.0.1:5000/download?name=../../etc/hostname"La concaténation produit /srv/downloads/../../etc/hostname. Le système de fichiers normalise ce chemin lors de la résolution et atteint /etc/hostname. Le contrôle is_file() ne protège rien : il vérifie seulement qu’un fichier existe à l’emplacement final, précisément celui que l’attaquant a choisi.
Le compromis central est net. Accepter des noms de fichiers fournis par le client semble commode pour l’interface et les intégrations anciennes ; cela transfère au code applicatif la responsabilité complète de la sémantique des chemins de chaque système d’exploitation. Cette responsabilité coûte cher et échoue souvent dans les détails. La recommandation est donc de ne jamais utiliser directement un nom fourni par le client comme référence d’autorisation.
Concaténation de chemin et sortie de périmètre
Section intitulée « Concaténation de chemin et sortie de périmètre »Un chemin relatif contient des composants interprétés par le système de fichiers. . désigne le répertoire courant, .. son parent. La séquence ../../etc/hostname demande donc deux remontées avant de descendre vers etc. Aucune permission spéciale n’est nécessaire pour cette navigation : seules les permissions sur le fichier final comptent.
La vulnérabilité apparaît dès que ces trois conditions sont réunies :
- une entrée contrôlée par le client influence un chemin ;
- l’application ajoute cette entrée à un répertoire supposé sûr ;
- l’application lit, écrit, supprime ou transmet ce chemin au système de fichiers.
Le problème ne concerne pas que les paramètres d’une requête Hypertext Transfer Protocol (HTTP). Les noms déposés dans un formulaire, les chemins inclus dans une archive, les identifiants de document issus d’une base de données compromise et les en-têtes de nom de fichier peuvent suivre la même route. Une valeur déjà validée à l’entrée ne reste pas fiable éternellement, surtout après une migration de données ou l’ajout d’un connecteur miraculeux vendu sur trois diapositives.
L’encodage ne corrige rien. %2e%2e%2f, doubles encodages, barres obliques inverses sous Windows, séparateurs multiples et caractères Unicode visuellement proches compliquent les filtres naïfs. Bloquer la chaîne ../ est donc insuffisant : une couche peut décoder avant une autre, ou normaliser selon des règles différentes. Un pare-feu applicatif Web, ou WAF (Web Application Firewall), peut repérer des charges grossières, mais il ne connaît pas le périmètre métier réel. Il doit compléter le contrôle applicatif, jamais le remplacer.
La vérification par préfixe textuel est une autre fausse bonne idée. Tester que str(candidate).startswith("/srv/downloads") accepte par exemple /srv/downloads-old/fichier. Les chemins sont des objets structurés, pas des chaînes décoratives. Traiter ce sujet avec des opérations de chaîne revient à réparer une serrure avec du ruban adhésif, ce qui reste du ruban adhésif avec une ambition immobilière.
Correctif : liste blanche, résolution et identifiant opaque
Section intitulée « Correctif : liste blanche, résolution et identifiant opaque »Une correction robuste combine trois contrôles, dans cet ordre conceptuel.
D’abord, accepter un nom logique très limité si un nom doit réellement transiter. Une expression régulière telle que [A-Za-z0-9][A-Za-z0-9._-]{0,127} interdit les séparateurs, les espaces ambigus et les composants de navigation. Cette règle doit correspondre au besoin métier : un service qui doit distribuer des fichiers internationaux ne doit pas prétendre le faire en filtrant arbitrairement les caractères, mais il ne doit pas non plus déléguer la grammaire des chemins au navigateur.
Ensuite, résoudre le chemin final et vérifier sa relation structurelle avec la racine autorisée. Path.resolve() suit les liens symboliques existants et supprime les composants . et ... La vérification doit intervenir après cette résolution. Une liste blanche seule ne couvre pas un lien symbolique placé dans le répertoire de téléchargement.
Enfin, préférer un identifiant opaque. L’interface transmet par exemple un identifiant aléatoire de document, et le serveur récupère le chemin interne depuis une base de données après contrôle d’autorisation. Le nom visible est alors une métadonnée de présentation, non une clé d’accès. C’est la solution à retenir pour tout service multi-utilisateur ou exposé sur réseau.
from pathlib import Pathimport refrom flask import abort, send_file
BASE = Path("/srv/downloads").resolve(strict=True)SAFE_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
def safe_download(name: str): # Rejeter avant toute opération sur le système de fichiers. if not SAFE_NAME.fullmatch(name): abort(400)
# Résoudre le fichier, y compris les liens symboliques existants. try: candidate = (BASE / name).resolve(strict=True) except FileNotFoundError: abort(404)
# Vérification structurelle, pas une comparaison de préfixe textuel. if not candidate.is_relative_to(BASE) or not candidate.is_file(): abort(404)
return send_file(candidate, as_attachment=True)Sous Python antérieur à 3.9, is_relative_to() n’existe pas. Il faut alors utiliser candidate.relative_to(BASE) dans un bloc try/except ValueError, pas réinventer une comparaison de chaînes.
Pour une écriture, le problème devient plus sévère. Entre la résolution et l’ouverture du fichier, un acteur capable de modifier le répertoire peut remplacer une entrée par un lien symbolique : c’est une condition de concurrence entre vérification et usage. Le répertoire de stockage ne doit donc jamais être inscriptible par des identités non fiables. Sur Linux, les écritures sensibles doivent employer des appels de système relatifs à un descripteur de répertoire, avec refus des liens symboliques. Les bibliothèques de haut niveau masquent souvent cette nuance avec une élégance de brochure commerciale.
Surface d’attaque et limites du contrôle
Section intitulée « Surface d’attaque et limites du contrôle »La lecture arbitraire peut exposer des fichiers de configuration, du code source, des journaux, des jetons de service, des clés privées ou des identifiants de fournisseur cloud. Lire /etc/passwd ne donne généralement pas accès aux mots de passe modernes, mais démontre une capacité de lecture hors périmètre. Cette capacité suffit souvent à récupérer ensuite un secret réellement exploitable.
Une path traversal d’écriture peut écraser une configuration, déposer un fichier interprété par un serveur Web ou altérer une tâche planifiée. L’impact dépend des droits du processus. Exécuter le service en administrateur transforme un défaut de validation en invitation à incendier la maison avec les allumettes rangées dans le placard.
Les contrôles décrits ne garantissent pas :
- l’autorisation métier de télécharger un document existant ;
- la confidentialité d’un fichier déjà exposé par une mauvaise règle d’accès ;
- la sécurité du contenu téléchargé contre un fichier malveillant ;
- l’isolation complète du processus compromis ;
- la protection contre une dépendance capable de lire le système de fichiers.
L’autorisation doit relier l’identité authentifiée, l’action demandée et l’identifiant opaque du document. Les principes de l’architecture cyber s’appliquent ici sans folklore : compte de service dédié, droits minimaux, stockage séparé des secrets et des contenus, puis segmentation réseau si le service n’a aucune raison d’atteindre d’autres ressources.
Un conteneur limite parfois le rayon d’explosion, mais ne rend pas une lecture arbitraire acceptable. Monter un volume de secrets dans le même conteneur qu’un serveur de fichiers vulnérable est une décision d’architecture, pas une fatalité imposée par Kubernetes.
Exploitation, supervision et reprise
Section intitulée « Exploitation, supervision et reprise »Les journaux doivent enregistrer l’identifiant de document demandé, le résultat d’autorisation, le code de réponse et un identifiant de corrélation. Enregistrer le chemin brut est acceptable seulement après encodage sûr pour empêcher l’injection de lignes dans les journaux. Il faut alerter sur les rejets répétés contenant des séparateurs, des composants de navigation ou des encodages anormaux, sans transformer chaque tentative isolée en sirène nucléaire.
Les secrets ne doivent pas résider dans le répertoire servi. Une clé privée, un jeton d’accès ou une sauvegarde de configuration placés sous une racine téléchargeable finissent disponibles par conception, même sans traversal. Les secrets exposés lors d’un incident doivent être révoqués et remplacés : corriger le code ne retire pas une copie déjà lue.
Les sauvegardes doivent préserver les données et leur inventaire d’identifiants, pas restaurer des permissions trop larges ou des liens symboliques inattendus. Après restauration, vérifier propriétaire, mode d’accès, montages et intégrité des métadonnées avant de remettre le service en circulation.
Les tests de sécurité doivent couvrir au minimum ../, les formes encodées, les séparateurs propres à la plateforme, les liens symboliques et les chemins voisins partageant le même préfixe. Le catalogue des failles applicatives rappelle une règle peu glamour : la validation doit être testée contre les représentations que les couches intermédiaires produisent réellement, pas seulement contre celles qu’un formulaire affiche.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans ces contrôles, le paramètre de téléchargement devient un lecteur générique du système de fichiers, avec les droits du processus. Sous Linux, les pseudo-fichiers de /proc/self (environ, cmdline) livrent les variables d’environnement et les arguments de lancement, souvent porteurs de jetons ou de chaînes de connexion, même quand aucun fichier de configuration n’existe sur disque. Si la clé de signature des sessions est lisible, elle permet de forger des cookies valides et d’usurper un compte sans jamais s’authentifier. Un fichier illisible fuit aussi : dans le code vulnérable, un fichier existant mais protégé provoque une erreur serveur au lieu d’un 404. La route devient alors un oracle d’existence qui permet de cartographier l’arborescence, les logiciels installés et les répertoires des comptes présents.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un serveur peut proposer le téléchargement d’un fichier en recevant son nom dans une demande. Pour trouver ce fichier, le programme ajoute ce nom à un dossier prévu pour les téléchargements. Le défaut apparaît lorsque le programme accepte aussi des indications permettant de remonter vers le dossier parent.
Le texte .. signifie « dossier parent ». Si le programme reçoit ../../etc/hostname, il construit d’abord un chemin qui semble commencer dans le dossier de téléchargement. Le système de fichiers interprète ensuite les deux remontées et arrive dans un autre dossier. Le serveur lit alors un fichier qui ne faisait pas partie de la liste prévue.
Bloquer seulement une écriture précise ne suffit pas, car le même caractère peut être transmis sous une autre forme puis décodé plus tard. La correction commence par accepter uniquement des noms limités à des lettres, chiffres et quelques signes définis. Le programme calcule ensuite le chemin final et vérifie qu’il reste réellement dans le dossier autorisé. Cette vérification doit aussi tenir compte des liens symboliques, qui peuvent rediriger vers un autre emplacement.
La meilleure organisation ne transmet pas de nom de fichier comme clé d’accès. Elle utilise un identifiant aléatoire associé, côté serveur, au vrai fichier et aux droits de la personne connectée. Si une tentative échoue, le serveur renvoie une erreur sans révéler la structure de ses dossiers.
À lire aussi
- Contourner un contrôle proxy par confusion de cheminDémonstration d’une attaque de confusion de préfixe de chemin entre un reverse proxy et un backend, avec ses contre-mesures.
- Comment empêcher une attaque de fixation de sessionDémonstration en laboratoire de l’attaque de fixation de session et contre-mesure par régénération de l’identifiant après authentification.
- Cross-site scripting (XSS) : mécanisme, variantes et défensesComprendre le cross-site scripting (XSS), ses variantes réfléchie, stockée, DOM et par mutation, et les défenses qui tiennent réellement en production.
- Comment un buffer overflow écrase la pileComprendre comment un débordement de tampon local modifie une pile d'exécution et peut détourner le retour d'une fonction.
Pages voisines
La citent
- Contourner un contrôle proxy par confusion de chemin
- Linux shell, permissions et processus, le modèle de contrôle d'accès
- Rust et la sécurité mémoire par construction, garanties et limites
Comment corriger une attaque path traversal
Elle cite
: les deux pages se citent l’une l’autre.
