Le path traversal existe partout où votre code utilise une entrée externe pour construire un chemin d'accès sans empêcher que ce chemin se résolve en dehors du répertoire prévu. CWE-22 est l'entrée qui le décrit. Recensez chacun de ces endroits, cessez de laisser les utilisateurs fournir des noms de fichiers et, pour les entrées qui subsistent, vérifiez le chemin résolu.
Ce que gagne un attaquant
La lecture n'est qu'un début. Selon ce que le code fait du chemin, un attaquant peut lire des fichiers inattendus et exposer des données sensibles, créer ou écraser des programmes et des bibliothèques pour exécuter du code, ou corrompre des fichiers jusqu'à ce que le produit cesse de fonctionner ou que les utilisateurs soient verrouillés. OWASP cite le code source de l'application et les fichiers de configuration parmi les fichiers accessibles.
Les fichiers protégés par un mécanisme de sécurité sont les pires cibles. Lire un fichier de mots de passe permet de mener des attaques par force brute sur les comptes, et y ajouter un nouveau compte peut contourner l'authentification.
La portée dépend de la plateforme. Sous Windows, un attaquant ne peut se déplacer qu'au sein de la partition qui contient la racine web, sous Linux sur l'ensemble du disque. Les contrôles d'accès du système d'exploitation continuent de s'appliquer en plus.
Repérer les endroits qui construisent des chemins
Partez du sink, pas de l'URL. Listez chaque appel qui ouvre, lit, sert, inclut ou écrit un fichier, puis remontez chaque argument de chemin jusqu'à son origine. Tout ce qui touche à la requête va sur la liste, y compris les appels cachés derrière vos propres fonctions d'encapsulation et les helpers de framework : vérifiez donc aussi leurs appelants.
L'entrée est plus large que la query string. Les exemples d'OWASP comprennent get-files.jsp?file=report.pdf, get-page.php?home=aaa.html et some-page.asp?page=index.html, mais aussi un cookie. Cet extrait est adapté de l'exemple d'OWASP (à l'origine de Wikipedia) :
$template = 'blue.php';
if ( isset( $_COOKIE['TEMPLATE'] ) )
$template = $_COOKIE['TEMPLATE'];
include ( "/home/users/phpguru/templates/" . $template );
Un cookie TEMPLATE=../../../../../../../../../etc/passwd fait remonter include() jusqu'au répertoire racine et renvoie le fichier de mots de passe. Les paramètres nommés file, page, template ou f sont les suspects habituels, mais les noms ne sont pas un critère fiable. Suivez les données.
Vérifiez trois formes :
- Traversée relative : les séquences
../qui sortent du répertoire prévu. - Chemins absolus :
?f=/etc/passwdou?f=/var/www/html/get.php, qui fonctionnent quand l'entrée est utilisée comme chemin complet. - Inclusions distantes :
?page=http://other-site/...lorsque le mécanisme d'inclusion accepte des URL.
Les messages d'erreur verbeux aident l'attaquant, car ils révèlent de vrais chemins sur lesquels s'appuyer. Vérifiez ce que renvoie votre application lorsqu'un fichier est absent.
Pourquoi les blocklists échouent
Supprimer ../ ne fonctionne pas, car la même traversée s'écrit de nombreuses façons :
%2e%2e%2f,..%2fet%2e%2e/signifient tous../...%5cet%2e%2e%5csignifient..\, ce qui compte sous Windows, où les deux séparateurs fonctionnent.%252e%252e%255cest la forme doublement encodée de..\. Les conteneurs web effectuent un seul niveau de décodage, de sorte qu'elle arrive encore encodée. Si votre code ou une bibliothèque la décode une seconde fois, elle devient une traversée après l'exécution du contrôle...%c0%afet..%c1%9csont d'autres encodages de../et..\.- Un octet nul, comme dans
?file=secret.doc%00.pdf, peut faire voir.pdfà une application Java alors que le système d'exploitation voit.doc. Les attaquants s'en servent pour contourner la validation.
Le filtrage se retourne aussi contre lui-même. Si l'on retire ../ de façon séquentielle dans .../...//, les caractères restants reforment ../. Filtrer uniquement / est inutile si le système de fichiers accepte \.
Première correction : cesser d'accepter des chemins
Le meilleur contrôle consiste à retirer l'entrée utilisateur du chemin. Utilisez des index plutôt que des fragments de noms de fichiers. L'exemple d'OWASP est un fichier de langue : l'utilisateur envoie 5 et le serveur fait correspondre cette valeur à la langue, au lieu de faire confiance à un nom renvoyé.
Pour les téléchargements, stockez les fichiers sous un ID et servez-les par ID, de sorte que l'utilisateur ne fournisse jamais ni nom ni extension. L'exemple du template devient :
$templates = [1 => 'blue.php', 2 => 'red.php'];
$id = (int) ($_COOKIE['TEMPLATE'] ?? 1);
$template = $templates[$id] ?? 'blue.php';
include '/home/users/phpguru/templates/' . $template;
Si vous devez absolument accepter un nom, validez-le par rapport à des valeurs connues et rejetez le reste. Utilisez une allowlist stricte de caractères, au plus un . dans le nom de fichier, aucun séparateur de répertoire, et une liste d'extensions autorisées. Rejetez les entrées invalides plutôt que d'essayer de les assainir.
Seconde correction : canonicaliser, puis vérifier
Lorsque l'entrée doit atteindre une API de fichiers, décodez-la une seule fois, puis résolvez-la avec une fonction de canonicalisation native avant de la valider :
| Langage | Fonction |
|---|---|
| C | realpath() |
| Java | getCanonicalPath() |
| ASP.NET | GetFullPath() |
| Perl | realpath() ou abs_path() |
| PHP | realpath() |
Ces fonctions suppriment les séquences .. et les liens symboliques. Après la résolution, vérifiez que le résultat se trouve toujours dans votre répertoire de base et refusez la requête dans le cas contraire. Un exemple en PHP, avec $name ayant déjà passé l'allowlist :
$base = realpath('/home/users/phpguru/templates');
$full = realpath($base . '/' . $name);
if ($full === false || strpos($full, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(404);
exit;
}
Conservez tous les contrôles côté serveur. La validation côté client se contourne trivialement.
Limiter les dégâts
Gardez les fichiers de configuration sensibles en dehors de la racine web. Sous Windows avec IIS, placez la racine web ailleurs que sur le disque système, afin que la traversée ne puisse pas atteindre les répertoires système. Les jails chrootées et les politiques d'accès au code restreignent les emplacements où des fichiers peuvent être lus ou écrits, et elles limitent les dégâts lorsqu'un contrôle a été oublié.
Pour les tests, rejouez les encodages ci-dessus sur chaque paramètre identifié, ainsi qu'un chemin absolu et une URL distante. Un test d'intrusion le fera de manière systématique.
Par où commencer
- Listez chaque appel de manipulation de fichiers dans votre code, ajoutez les fonctions d'encapsulation et les helpers de framework qui les entourent, et remontez chaque argument de chemin jusqu'à sa source, y compris les cookies et les en-têtes.
- Remplacez les noms de fichiers fournis par l'utilisateur par des ID associés à des fichiers sur le serveur, en commençant par les téléchargements et la sélection de templates ou de langues.
- Pour tout cas restant, décodez une seule fois, canonicalisez avec la fonction de la plateforme et rejetez tout ce qui se résout en dehors du répertoire de base.
- Supprimez le code de blocklist qui retire
../ou/, et remplacez-le par une allowlist de caractères, d'extensions et, si possible, de valeurs. - Déplacez la configuration sensible hors de la racine web et cessez de renvoyer des messages d'erreur qui révèlent des chemins de fichiers.
- Testez chaque paramètre avec les variantes encodées, doublement encodées, absolues et distantes, et conservez ces cas comme tests de non-régression.
