Prévention des attaques par traversée de chemin dans Node.js
Si votre application Express sert des fichiers en fonction de données saisies par l’utilisateur — une route de téléchargement, un aperçu de fichier uploadé, une ressource dynamique — vous avez une vulnérabilité potentielle de traversée de chemin. C’est l’une des erreurs de sécurité les plus courantes et les plus dommageables concernant les fichiers dans Node.js, et il est étonnamment facile de l’introduire sans s’en rendre compte.
Cet article explique comment fonctionnent les attaques par traversée de chemin, pourquoi les « correctifs » courants sont insuffisants, et à quoi ressemble réellement une gestion sécurisée des fichiers dans Node.js en pratique.
Points clés à retenir
- Les attaques par traversée de chemin exploitent une validation manquante ou incomplète des chemins de fichiers fournis par l’utilisateur, permettant aux attaquants d’accéder à des fichiers en dehors des répertoires prévus.
path.normalize()etpath.join()ne sont pas des outils de sécurité — ils nettoient les chemins mais n’empêchent pas la traversée de répertoires.- La défense correcte consiste à résoudre les chemins avec
path.resolve()et à vérifier que le résultat reste dans un répertoire de base autorisé en utilisant une vérificationstartsWithqui inclutpath.sep. - L’approche la plus sûre consiste à éviter complètement les chemins fournis par l’utilisateur en mappant les entrées aux fichiers via une recherche basée sur des identifiants.
Qu’est-ce qu’une attaque par traversée de chemin ?
Une attaque par traversée de chemin (également appelée traversée de répertoire) se produit lorsqu’un attaquant manipule une entrée de chemin de fichier pour accéder à des fichiers en dehors du répertoire prévu. L’exemple classique :
GET /download?file=../../etc/passwd
Si votre serveur joint naïvement cette entrée avec un répertoire de base et lit le fichier, vous venez de livrer le fichier de mots de passe de votre système.
L’attaque n’est pas sophistiquée. Elle exploite une validation d’entrée manquante ou incomplète — plus précisément, l’échec de vérifier qu’un chemin résolu reste dans un répertoire autorisé.
Pourquoi path.normalize() et path.join() ne suffisent pas
C’est l’élément le plus important à comprendre concernant la prévention de la traversée de chemin dans Node.js : ces utilitaires ne sont pas des outils de sécurité.
path.normalize() résout les séquences .. et nettoie les barres obliques redondantes. path.join() concatène des segments. Aucun des deux n’empêche la traversée — ils produisent simplement un chemin plus propre qui peut toujours pointer en dehors de votre répertoire prévu.
Considérez ce motif courant mais non sécurisé :
// ❌ Non sécurisé : path.join n'empêche pas la traversée
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.query.file)
res.sendFile(filePath)
})
Si req.query.file est ../../etc/passwd, path.join() le résout proprement — et envoie le mauvais fichier.
L’encodage d’URL aggrave la situation. Un attaquant pourrait envoyer ..%2F..%2Fetc%2Fpasswd. Express décode automatiquement les paramètres d’URL, vous devez donc valider la valeur décodée que vous recevez. Si vous gérez vous-même les URL brutes, décodez une fois (avec une gestion d’erreur appropriée) avant la validation.
Le motif correct : résoudre et vérifier le confinement
L’approche fiable pour une gestion sécurisée des fichiers dans Node.js est :
- Résoudre le chemin fourni par l’utilisateur par rapport à votre répertoire de base en utilisant
path.resolve(). - Vérifier que le chemin résolu reste dans ce répertoire.
const path = require('path')
const BASE_DIR = path.resolve(__dirname, 'uploads')
function safeResolve(userInput) {
const resolved = path.resolve(BASE_DIR, userInput)
// S'assurer que le chemin résolu est à l'intérieur de BASE_DIR
if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
return null
}
return resolved
}
// ✅ Route de téléchargement de fichier Express sécurisée
app.get('/download', (req, res) => {
const safePath = safeResolve(req.query.file)
if (!safePath) {
return res.status(403).send('Accès refusé')
}
res.sendFile(safePath)
})
Notez l’ajout de path.sep. Sans cela, un répertoire de base de /uploads permettrait incorrectement à /uploads-other/secret.txt de passer la vérification de préfixe.
Sous Windows, path.resolve() et path.sep gèrent correctement les barres obliques inversées, donc ce motif fonctionne sur toutes les plateformes. Pour une validation plus stricte, vous pouvez également comparer les chemins en utilisant path.relative() et rejeter tout résultat qui échappe au répertoire de base.
Sachez que les liens symboliques peuvent toujours pointer en dehors de votre répertoire de base. Si vous servez des fichiers sensibles, résolvez les chemins réels avec fs.realpath() avant de les envoyer.
Discover how at OpenReplay.com.
Mieux encore : éviter complètement les chemins fournis par l’utilisateur
L’approche la plus sécurisée consiste à ne pas utiliser du tout les chemins fournis par l’utilisateur. Au lieu de cela, mappez les entrées utilisateur aux fichiers de manière indirecte :
// ✅ Recherche de fichier basée sur ID — aucune construction de chemin à partir d'entrée utilisateur
const FILES = {
'report-2024': 'reports/annual-2024.pdf',
'invoice-001': 'invoices/inv-001.pdf',
}
app.get('/download/:id', (req, res) => {
const filePath = FILES[req.params.id]
if (!filePath) {
return res.status(404).send('Non trouvé')
}
res.sendFile(path.resolve(__dirname, 'secure', filePath))
})
Cela élimine entièrement le risque de traversée. Si votre cas d’usage le permet — téléchargements de fichiers, exports de documents, ressources spécifiques à l’utilisateur — c’est le motif à privilégier en premier lieu.
Une note sur le service de fichiers statiques Express
express.static() est généralement plus sûr pour servir des ressources publiques car il restreint l’accès à un répertoire racine défini. Le risque apparaît lorsque vous écrivez des routes personnalisées de service de fichiers qui construisent des chemins à partir de paramètres de requête ou exposent des répertoires non prévus. La configuration et la sélection de la racine restent importantes.
Conclusion
La traversée de chemin est une vulnérabilité simple avec un correctif simple : ne jamais faire confiance aux entrées utilisateur en tant que chemin de fichier. Résolvez-le par rapport à une base fixe, vérifiez le confinement avec path.resolve() et une vérification startsWith qui inclut path.sep, et validez l’entrée décodée avant utilisation. Lorsque c’est possible, privilégiez les recherches basées sur des identifiants qui contournent la construction de chemin à partir d’entrées utilisateur. path.normalize() et path.join() sont des utilitaires utiles — mais pas des garanties de sécurité.
FAQ
Cela aide en servant des fichiers depuis un répertoire racine défini et en gérant la résolution de chemin en interne, mais ce n'est pas une garantie de sécurité complète. Une mauvaise configuration ou l'exposition de répertoires sensibles peuvent toujours créer un risque. Les routes personnalisées de service de fichiers qui utilisent des entrées utilisateur sont là où la plupart des vulnérabilités se produisent.
Non. Filtrer ou rejeter les chaînes qui contiennent des séquences point-point est fragile. Les attaquants peuvent contourner ces vérifications avec l'encodage d'URL, le double encodage, ou des séparateurs de chemin spécifiques à la plateforme. La défense fiable consiste à résoudre le chemin complet avec path.resolve() puis à vérifier que le résultat se trouve dans votre répertoire de base prévu en utilisant une vérification startsWith ou path.relative().
Oui. L'utilisation de path.resolve() et path.sep fait fonctionner la vérification de confinement sur toutes les plateformes. path.resolve() normalise les chemins Windows, et path.sep s'évalue au séparateur correct. Pour une validation plus stricte, utilisez path.relative() pour vous assurer que le chemin résolu ne s'échappe pas du répertoire de base.
Oui. Après avoir confirmé que le chemin résolu est dans votre répertoire de base, vérifiez que le fichier existe en utilisant fs.existsSync() ou fs.access() avant d'appeler res.sendFile(). Cela empêche de divulguer des informations sur la structure de votre répertoire via des messages d'erreur et vous donne le contrôle sur la réponse lorsqu'un fichier est manquant.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.