Injection - Path Traversal
La traversée de répertoire est un autre type courant de vulnérabilité par injection. Elle survient généralement lorsque la construction d'un URI (qu'il s'agisse d'une URL, d'un chemin de fichier ou autre) ne garantit pas correctement que le chemin final résolu ne pointe pas en dehors de la racine du chemin prévu.
Il est important de noter que la traversée de répertoire peut également être considérée comme une vulnérabilité par *injection* de chemin.
L'impact d'une vulnérabilité de traversée de répertoire dépend fortement du contexte dans lequel elle se produit et des mesures de sécurité globales mises en place. Mais avant d'aborder ces aspects, examinons un exemple pratique pour illustrer cette vulnérabilité :
Analyse rapide
Imaginez un point de terminaison dans votre application qui sert des documents, tels que des modèles de contrats ou des offres d'emploi. Il peut s'agir de fichiers statiques, comme des PDF, stockés dans votre application.
Dans ce cas, vous pourriez avoir un code similaire à celui-ci pour récupérer les fichiers à la demande :
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
return file.read(path);
Pour démontrer le fonctionnement de la vulnérabilité, nous devons connaître la racine de notre application. Pour cet exemple, supposons que la racine soit « /var/www/api/ ».
Nous savons que l'application utilise un paramètre « filename ». Examinons quelques exemples d'entrées et leurs résultats :
Remarquez comment nous pouvons parcourir le système de fichiers en utilisant « ../ ». Nous sommes capables de sortir du dossier « documents » où se trouvent habituellement les PDF pour accéder au dossier « /etc/ » contenant le fichier « shadow » qui, sous Linux, renferme les hachages de mots de passe. Comme vous pouvez l'imaginer, ce n'est vraiment pas idéal.
La traversée dans les URL
Une autre variante de la traversée de répertoire peut se produire lors de la construction d'URL destinées à interagir avec une API. Supposons que nous ayons une API avec les méthodes suivantes :
L'API est sollicitée par une autre application qui pourrait l'appeler, par exemple, pour obtenir des informations sur une commande :
let apiBase = "https://my.api/api/v1";
let orderApi = apiBase + "/order/get";
let apiUrl = orderApi + request.params.orderId;
let response = http.get(apiUrl);
Que se passe-t-il maintenant, selon l'identifiant de commande fourni par l'utilisateur ? Vous verrez ci-dessous l'URL effective appelée en fonction de l'entrée fournie.
La canonisation n'est généralement pas effectuée côté client (bien que cela soit possible), mais les serveurs web canoniseront la requête dans le format présenté ci-dessous.
Avec l'entrée du second exemple, au lieu de récupérer la commande portant l'identifiant « 1 », nous avons en réalité invoqué la méthode de suppression, ce qui entraîne, bien entendu, la suppression de la commande.
Mesures d'atténuation
Lorsqu'on aborde le problème du parcours de répertoire (path traversal), il existe des mesures d'atténuation directes ainsi que des techniques de défense indirectes qui peuvent, et devraient, être appliquées aussi souvent que possible. Commençons par examiner comment gérer les chemins d'accès.
Atténuation directe
Lorsqu'il s'agit de gérer un chemin d'accès, il est essentiel de comprendre le processus de résolution de chemin, ou canonisation de chemin, et son importance.
Un chemin comme « /var/www/api/documents/../../../../etc/shadow » est un chemin non canonique. Si vous demandez ce chemin à votre système de fichiers, il le canonisera en « /etc/shadow ». Il est crucial de ne jamais tenter d'ouvrir des chemins non canoniques. Vous devez plutôt canoniser les chemins au préalable, vérifier qu'ils pointent uniquement vers le fichier ou le dossier prévu, puis les lire.
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
let resolvedPath = path.resolve(path);
if(!resolvedPath.startswith(baseFolder))
return "Tentative de lecture en dehors du dossier de base";
else
return file.read(resolvedPath);
Anti-modèle : tenter de nettoyer les noms de fichiers
Il peut être tentant de faire quelque chose comme ceci :
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename.replace("../", "");
...
Cependant, cette approche ne doit pas être utilisée. La clé dans la gestion des chemins est de toujours examiner le chemin canonique.
Tant que le chemin canonique ne contrevient à aucune règle, la manière dont le chemin est construit au final n'a aucune importance. Tenter de nettoyer un chemin de cette façon est très sujet aux erreurs et rarement sécurisé, voire jamais.
Limiter l'accès
Dans nos exemples précédents, nous avons utilisé la lecture du fichier « /etc/shadow », qui contient les hachages de mots de passe sous Linux. Mais il n'y a aucune raison pour qu'une application puisse lire ce fichier, ou d'autres fichiers, en dehors de sa racine.
Si vous utilisez des conteneurs, vous atténuez probablement déjà de nombreux risques. Il est vital de prendre des mesures pour renforcer le conteneur (ne pas l'exécuter en tant que root, par exemple). Il est fortement recommandé de supprimer tous les privilèges de votre processus web et de limiter ses autorisations de lecture sur le système de fichiers aux seuls fichiers strictement nécessaires.
Exemples
Nous allons maintenant partager quelques exemples dans différents langages pour mieux illustrer ces concepts en action.
C# - Non sécurisé
Le fait de ne pas résoudre le chemin complet, ou de ne pas s'assurer que vous utilisez uniquement la partie nom de fichier d'un chemin, rend le code vulnérable au parcours de répertoire.
var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";
// NON SÉCURISÉ : Lit /etc/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));
C# - Sécurisé - canonique
Dans cet exemple, nous nous protégeons contre le parcours de répertoire en résolvant le chemin complet (absolu) et en nous assurant que le chemin résolu se trouve bien à l'intérieur de notre dossier de base.
var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";
var canonicalPath = Path.GetFullPath(Path.Combine(baseFolder, fileName));
// SÉCURISÉ : rejette toute tentative de lecture en dehors du dossier de base spécifié.
if(!canonicalPath.StartsWith(baseFolder))
return "Tentative de lecture d'un fichier en dehors du dossier de base";
var fileContents = File.ReadAllText(canonicalPath);
C# - Sécurisé - nom de fichier
Dans cet exemple, nous nous protégeons contre le parcours de répertoire (Path Traversal) en ne conservant que la partie nom de fichier du chemin, garantissant ainsi qu'il est impossible de sortir du dossier spécifié.
var baseFolder = "/var/www/app/documents/";
// À utiliser uniquement si vous n'autorisez pas la navigation dans d'autres sous-dossiers
var fileName = Path.GetFileName("../../../../../etc/passwd");
// SÉCURISÉ : lit /var/www/app/documents/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));
Java - Non sécurisé
Le fait de ne pas résoudre le chemin complet, ou de ne pas s'assurer que vous utilisez uniquement la partie nom de fichier d'un chemin, rend le code vulnérable au parcours de répertoire.
String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";
// NON SÉCURISÉ : lit /etc/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);
Java - Sécurisé - Chemin canonique
Dans cet exemple, nous nous protégeons contre le parcours de répertoire en résolvant le chemin complet (absolu) et en nous assurant que le chemin résolu se trouve bien à l'intérieur de notre dossier de base.
String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";
// NON SÉCURISÉ : lit /etc/passwd
Path normalizedPath = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
return "Tentative de lecture d'un chemin en dehors de la racine";
}
else
{
List<String> lines = Files.readAllLines(normalizedPath);
}
Java - Sécurisé - Nom de fichier
Dans cet exemple, nous nous protégeons contre le parcours de répertoire (Path Traversal) en ne conservant que la partie nom de fichier du chemin, garantissant ainsi qu'il est impossible de sortir du dossier spécifié.
String baseFolder = "/var/www/app/documents/";
// À utiliser uniquement si vous n'autorisez pas la navigation dans d'autres sous-dossiers
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();
// SÉCURISÉ : lit /var/www/app/documents/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);
Javascript - Non sécurisé
Le fait de ne pas résoudre le chemin complet, ou de ne pas s'assurer que vous utilisez uniquement la partie nom de fichier d'un chemin, rend le code vulnérable au parcours de répertoire.
const fs = require('fs');
const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";
// NON SÉCURISÉ : lit /etc/passwd
const data = fs.readFileSync(baseFolder + fileName, 'utf8');
Javascript - Sécurisé - Chemin canonique
Dans cet exemple, nous nous protégeons contre le parcours de répertoire en résolvant le chemin complet (absolu) et en nous assurant que le chemin résolu se trouve bien à l'intérieur de notre dossier de base.
const fs = require("fs");
const path = require("path");
const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";
const normalizedPath = path.normalize(path.join(baseFolder, fileName));
// SÉCURISÉ : lit /var/www/app/documents/passwd
const data = fs.readFileSync(normalizedPath, 'utf8');
Javascript - Sécurisé - Nom de fichier
Dans cet exemple, nous nous protégeons contre le parcours de répertoire (Path Traversal) en ne conservant que la partie nom de fichier du chemin, garantissant ainsi qu'il est impossible de sortir du dossier spécifié.
const fs = require("fs");
const path = require("path");
const baseFolder = "/var/www/app/documents/";
const fileName = path.basename("../../../../../etc/passwd");
// SÉCURISÉ : Lit /var/www/app/documents/passwd
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');
Python - Non sécurisé
Le fait de ne pas résoudre le chemin complet, ou de ne pas s'assurer que vous utilisez uniquement la partie nom de fichier d'un chemin, rend le code vulnérable au parcours de répertoire.
baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"
# NON SÉCURISÉ : Lit /etc/passwd
fileContents = open(baseFolder + fileName).read()
Python - Sécurisé - Canonique
Dans cet exemple, nous nous protégeons contre le parcours de répertoire en résolvant le chemin complet (absolu) et en nous assurant que le chemin résolu se trouve bien à l'intérieur de notre dossier de base.
import os.path
baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"
normalizedPath = os.path.normpath(baseFolder + fileName)
# SÉCURISÉ : Rejette toute tentative de lecture hors du dossier de base
if not normalizedPath.startswith(baseFolder):
return "Tentative de lecture hors du dossier de base"
# SÉCURISÉ : Lit /var/www/app/documents/passwd
fileContents = open(normalizedPath).read()
Python - Sécurisé - Nom de fichier
Dans cet exemple, nous nous protégeons contre le parcours de répertoire (Path Traversal) en ne conservant que la partie nom de fichier du chemin, garantissant ainsi qu'il est impossible de sortir du dossier spécifié.
import os.path
baseFolder = "/var/www/app/documents/"
fileName = os.path.basename("../../../../../etc/passwd")
# SÉCURISÉ : Lit /var/www/app/documents/passwd
fileContents = open(os.path.join(baseFolder, fileName)).read()
Le codage sécurisé en pratique
Explorez nos directives de développement sécurisé pour comprendre et atténuer les vulnérabilités logicielles telles que l'OWASP Top 10, et plongez dans des missions de formation guidées pour vous exercer sur des simulations d'applications réelles.