In this article
Gestion sécurisée des chemins : pourquoi sécuriser les opérations sur le système de fichiers est plus difficile qu’il n’y paraît
Les vulnérabilités de traversée de chemins continuent de faire surface. Les fonctions d’E/S de fichiers des bibliothèques standard, fournies par la plupart des langages, ont été conçues pour être pratiques, pas pour résister à des environnements hostiles. Lorsque votre code s’exécute dans un bac à sable, sur un système partagé ou avec des entrées non fiables, cette lacune devient une violation de la frontière de sécurité.
L’équipe de recherche en sécurité de Snyk a rencontré ce problème à maintes reprises dans différents écosystèmes :
Des vulnérabilités critiques dans Incus combinant injection de saut de ligne et attaques par lien symbolique pour permettre l’écriture arbitraire de fichiers et une élévation de privilèges jusqu’à root sur l’hôte
Une chaîne d’élévation de privilèges dans NixOS exploitant des conditions de concurrence dans le magasin Nix pour conserver un accès en écriture à des chemins qui auraient dû être immuables
Une élévation de privilèges dans Ubuntu 24.04, de l’utilisateur par défaut à root, en enchaînant la manipulation du système de fichiers et des bugs de composants privilégiés
Il ne s’agit pas de techniques d’attaque exotiques, mais plutôt de code qui vérifie un chemin à une étape et l’utilise à une autre, laissant entre les deux un intervalle pendant lequel un attaquant peut remplacer une cible sûre par une cible malveillante.
Les trois catégories de vulnérabilités
Traversée de chemins
Le grand classique. Une entrée fournie par l’utilisateur, comme ../../etc/passwd, sort du répertoire prévu. La plupart des développeurs savent qu’il faut s’en méfier, mais ce problème revient régulièrement, car la validation des chemins est plus complexe qu’il n’y paraît. De simples vérifications de chaînes ne tiennent pas compte de l’encodage d’URL, des octets nuls, de la normalisation Unicode ni des séparateurs de chemin propres à chaque plateforme.
# Looks safe, isn't safe
def read_user_file(filename):
path = os.path.join("/app/uploads", filename)
if not path.startswith("/app/uploads"):
raise ValueError("Access denied")
return open(path).read()
# filename = "../../etc/passwd"
# path = "/app/uploads/../../etc/passwd"
# os.path.join resolves this, but startswith still passes on some inputsAttaques par lien symbolique
Un attaquant crée un lien symbolique qui redirige d’un emplacement auquel votre code est censé accéder vers un emplacement auquel il ne devrait pas accéder. Si votre code suit les liens symboliques (comportement par défaut sur la plupart des systèmes), il lit ou écrit dans la cible choisie par l’attaquant.
C’est particulièrement dangereux dans les environnements partagés, les répertoires temporaires et aux frontières entre conteneurs et bacs à sable, où un attaquant peut disposer d’un accès en écriture limité au système de fichiers.
Conditions de concurrence TOCTOU
Time-of-Check to Time-of-Use (vérification-utilisation). Votre code valide un chemin, puis l’utilise. Entre ces deux opérations, un attaquant remplace la cible. La vérification réussit, mais l’opération porte sur un autre fichier.
# Classic TOCTOU vulnerability
if os.path.isfile(path) and not os.path.islink(path):
# Attacker swaps path for a symlink right here
with open(path, 'w') as f:
f.write(data) # Now writing to attacker's targetLe problème fondamental est que les opérations reposant sur des chemins sont intrinsèquement sujettes aux conditions de concurrence sur les systèmes multi-utilisateurs ou multiprocessus. L’état du système de fichiers peut changer entre deux appels système faisant référence au même chemin.
La solution : les opérations basées sur des descripteurs de fichiers
La bonne approche combine openat(), O_NOFOLLOW et la famille d’appels système f* (fstat, fchmod, fchown). L’idée clé : dès que vous disposez d’un descripteur de fichier ouvert, celui-ci fait référence à un objet précis du système de fichiers, quels que soient les changements apportés au chemin.
Voici comment procéder :
Commencez par un répertoire connu comme sûr (par exemple
/ou la racine d’un bac à sable)Ouvrez chaque composant du chemin individuellement avec
openat(), en utilisant le descripteur de fichier du répertoire précédentUtilisez
O_NOFOLLOWpour rejeter les liens symboliques à chaque étapeValidez chaque composant (pas de
.., aucun lien symbolique, autorisations correctes)Utilisez le descripteur de fichier final pour toutes les opérations suivantes
Cette méthode élimine les risques TOCTOU, car vous ne réutilisez jamais le chemin après l’ouverture. Chaque opération passe par le descripteur de fichier, qui, selon les garanties du noyau, fait référence au même inode quels que soient les changements de chemin.
Prise en charge selon les langages
C/C++ : contrôle total
C et C++ donnent directement accès à openat() et à O_NOFOLLOW, ce qui rend l’implémentation correcte simple (même si elle est verbeuse). La solution de Google pour ChromeOS (libbrillo/brillo/files/safe_fd.cc) constitue un bon exemple d’implémentation.
int safe_open(int dirfd, const char *component) {
return openat(dirfd, component,
O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
}Go : prise en charge dans la bibliothèque standard (depuis Go 1.24)
Go a ajouté os.Root à sa bibliothèque standard. Cette fonctionnalité permet d’accéder en toute sécurité, sans condition de concurrence, aux fichiers d’une arborescence de répertoires. C’est la référence en matière de prise en charge au niveau du langage.
// Go 1.24+ — safe by default
root, err := os.OpenRoot("/sandbox")
if err != nil {
log.Fatal(err)
}
defer root.Close()
f, err := root.Open("user/data.txt")
// Guaranteed to stay within /sandbox, TOCTOU-proofL’article du blog de Go sur la conception de os.Root mérite d’être lu pour comprendre les raisons de ces choix et les cas limites pris en compte.
Rust : de bonnes primitives, à assembler manuellement
Rust expose openat via la crate nix et prend en charge O_NOFOLLOW. La crate cap-std fournit un accès au système de fichiers basé sur les capacités, qui empêche par conception la traversée de chemins.
use cap_std::fs::Dir;
let root = Dir::open_ambient_dir("/sandbox", cap_std::ambient_authority())?;
let file = root.open("user/data.txt")?;
// Constrained to /sandboxPython : prise en charge partielle
Python 3.11 et versions ultérieures proposent os.open() avec les paramètres O_NOFOLLOW et dir_fd, mais la mise en place d’une méthode complète et sûre de parcours des chemins nécessite un travail manuel. La bibliothèque standard ne propose aucun équivalent à os.Root de Go.
import os
def safe_open_under(root_path, relative_path):
root_fd = os.open(root_path, os.O_RDONLY | os.O_DIRECTORY)
try:
components = relative_path.split('/')
current_fd = root_fd
for component in components[:-1]:
if component in ('', '.', '..'):
raise ValueError(f"Invalid path component: {component}")
next_fd = os.open(
component,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current_fd
)
if current_fd != root_fd:
os.close(current_fd)
current_fd = next_fd
return os.open(
components[-1],
os.O_RDONLY | os.O_NOFOLLOW,
dir_fd=current_fd
)
finally:
os.close(root_fd)Node.js : pratiquement impossible ?
C’est là que les choses se compliquent. La bibliothèque standard de Node.js n’expose ni openat() ni O_NOFOLLOW. Le module fs fonctionne uniquement avec des chemins, et non avec des descripteurs de fichiers. Sans modules natifs, il est impossible d’effectuer un parcours de répertoires sécurisé contre les risques TOCTOU en Node.js pur.
La meilleure solution consiste à appeler fs.realpath(), puis à vérifier le préfixe, mais cette méthode reste intrinsèquement sujette aux conditions de concurrence. Un module natif utilisant N-API pourrait exposer openat(), mais cela impose des exigences de compilation et ajoute une complexité propre à chaque plateforme.
Il s’agit d’une lacune importante pour un environnement d’exécution très utilisé dans les outils CLI, les systèmes de build et les environnements conteneurisés, où la sécurité des chemins est essentielle.
Dans quels cas est-ce important ?
Toutes les applications n’ont pas besoin d’opérations sur les fichiers sécurisées contre les risques TOCTOU. Si votre code n’ouvre que des fichiers à partir de chemins codés en dur ou ne s’exécute que dans un environnement mono-utilisateur sans entrées non fiables, les fonctions de la bibliothèque standard conviennent.
La gestion sécurisée des chemins doit vous préoccuper si :
Votre code s’exécute dans un bac à sable ou un conteneur, et les opérations sur le système de fichiers franchissent des frontières de confiance
Votre code traite des chemins de fichiers fournis par les utilisateurs (téléversements, fichiers de configuration, répertoires de plug-ins)
Votre code s’exécute avec des privilèges élevés et accède à des chemins influencés par des utilisateurs non privilégiés
Votre code fonctionne dans des environnements partagés (systèmes multilocataires, pipelines CI/CD, systèmes de build)
Votre code gère des fichiers temporaires dans des répertoires accessibles en écriture à tous, comme
/tmp
Mesures recommandées
Auditez votre base de code pour repérer les opérations sur les fichiers qui utilisent des chemins et prennent des entrées utilisateur
Remplacez les vérifications basées sur les chemins (comme
startswith()aprèsrealpath()) par un parcours reposant sur des descripteurs de fichiers, dans la mesure du possibleAvec Go 1.24 et les versions ultérieures, utilisez
os.Rootpour tout accès à des fichiers dans un bac à sableEn Rust, envisagez la crate
cap-stdpour accéder au système de fichiers avec des capacitésEn Node.js, tenez compte de cette limite et mettez en place une défense en profondeur (validation des entrées, chroot, profils AppArmor/SELinux)
Utilisez Snyk Code pour détecter les schémas de traversée de chemins dans votre base de code pendant le développement
Des failles cachées dans la gestion des fichiers, comme la traversée de chemins, exposent-elles discrètement vos applications à des compromissions plus graves ? Téléchargez le livre blanc The AI Security Crisis in Your Python Environment pour découvrir comment les équipes modernes préviennent ces risques dans le code généré par l’IA comme dans celui écrit par les développeurs.
LIVRE BLANC
La crise de la sécurité de l’IA dans votre environnement Python
Avec l’accélération fulgurante du développement, savez-vous vraiment à quoi votre environnement d’IA peut accéder ?