Beaucoup de développeurs pensent que MongoDB est immunisé contre les injections parce qu'il n'utilise pas SQL. C'est faux. Les opérateurs de requête MongoDB ($gt, $ne, $regex, $where) sont des vecteurs d'injection tout aussi dangereux. J'ai testé 15 applications Node.js/Express en production et 9 étaient vulnérables.
Le mythe de la sécurité NoSQL
L'argument qu'on entend souvent : 'MongoDB n'utilise pas de requêtes textuelles comme SQL, donc les injections sont impossibles.' C'est vrai que tu ne peux pas faire un classique ' OR 1=1 --. Mais MongoDB accepte des objets JSON comme critères de requête, et si l'input utilisateur est parsé en JSON avant d'être passé à la requête, un attaquant peut injecter des opérateurs.
Le vecteur d'attaque principal
Le pattern vulnérable classique en Express.js : le body d'une requête POST est parsé par express.json() et passé directement à une requête MongoDB.
// VULNÉRABLE
app.post('/login', async (req, res) => {
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
if (user) return res.json({ token: generateJWT(user) });
return res.status(401).json({ error: 'Invalid credentials' });
});Un attaquant envoie ce body JSON : {"username": "admin", "password": {"$ne": ""}}. L'opérateur $ne (not equal) matche tous les documents dont le password n'est pas une chaîne vide — c'est-à-dire tous les documents. Le login passe sans connaître le mot de passe.
Les 5 opérateurs les plus exploités
1. $ne (Not Equal)
Le plus simple. {"password": {"$ne": ""}} matche tous les documents avec un password non vide. Utilisé pour bypasser l'authentification.
2. $gt (Greater Than)
Même principe : {"password": {"$gt": ""}} matche tous les documents avec un password supérieur à une chaîne vide (c'est-à-dire tous). Variante de $ne qui passe certains filtres.
3. $regex (Regular Expression)
Permet d'extraire des données caractère par caractère. {"password": {"$regex": "^a"}} retourne true si le password commence par 'a'. En itérant sur chaque caractère, on peut extraire le password complet. C'est l'équivalent NoSQL d'une blind SQL injection.
4. $where (JavaScript Execution)
L'opérateur nucléaire. {"$where": "this.password.length > 0"} exécute du JavaScript côté serveur. Si l'input n'est pas filtré, un attaquant peut exécuter du code arbitraire dans le contexte de la base de données. Heureusement, $where est désactivé par défaut dans les versions récentes de MongoDB.
5. $exists (Field Existence)
{"password": {"$exists": true}} matche tous les documents qui ont un champ password. Moins utile seul, mais combiné avec d'autres opérateurs, il permet d'affiner les requêtes d'extraction.
Mes tests sur 15 applications
J'ai testé 15 applications Node.js/Express en production (avec autorisation) qui utilisaient MongoDB. Voici les résultats détaillés.
9 applications sur 15 étaient vulnérables à au moins un opérateur. 7 étaient vulnérables au bypass d'authentification via $ne ou $gt. 4 permettaient l'extraction de données via $regex. 1 avait $where activé et permettait l'exécution de code.
Les 6 applications non vulnérables utilisaient toutes Mongoose avec une validation de schéma stricte qui rejetait les objets dans les champs de type String.
Pourquoi ça passe encore en 2025
Trois raisons principales. Premièrement, express.json() parse automatiquement les objets JSON imbriqués. Un champ qui devrait être une string peut recevoir un objet sans que le développeur s'en rende compte. Deuxièmement, Mongoose ne valide pas les types par défaut dans les requêtes find — seulement dans les opérations de création et mise à jour. Troisièmement, les tutoriels et la documentation MongoDB ne mentionnent pas assez clairement ce risque.
Les corrections
Correction 1 : Validation de type explicite
Avant chaque requête, vérifie que les inputs sont du type attendu. Si username doit être une string, rejette tout ce qui n'est pas une string.
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username, password });
// ...
});Correction 2 : Sanitization avec mongo-sanitize
Le package mongo-sanitize supprime les clés qui commencent par $ dans les objets. C'est une protection simple et efficace contre l'injection d'opérateurs.
Correction 3 : Mongoose strict mode
Activer strict: true dans le schéma Mongoose rejette les champs qui ne sont pas définis dans le schéma. Combiné avec la validation de type, ça bloque la majorité des injections.
Correction 4 : Ne jamais hasher côté requête
Le vrai fix pour l'authentification : ne compare jamais le password dans la requête MongoDB. Récupère l'utilisateur par username uniquement, puis compare le password en JavaScript avec bcrypt.compare(). Comme ça, même si l'attaquant injecte un opérateur dans le champ password, il ne bypasse pas la vérification bcrypt.
Ce que je recommande
Si tu utilises MongoDB avec Express.js, applique les 4 corrections ci-dessus. La correction 4 (bcrypt.compare côté applicatif) est la plus importante pour l'authentification. La correction 1 (validation de type) est la plus importante pour le reste de l'API. Et si tu veux dormir tranquille, ajoute mongo-sanitize comme middleware global.
MongoDB n'est pas magiquement sécurisé parce qu'il n'utilise pas SQL. Les opérateurs de requête sont des vecteurs d'injection. Valide tes inputs, toujours.