NoSQL injection
Backend Fundamentals · Backend Security · 8 min read
1. What problem does this solve?
Motivation
When a backend receives user data and uses it to build a query against a NoSQL database (MongoDB, Couchbase, DynamoDB, etc.), the developer often concatenates or merges the input directly into the query object. If the input contains special characters or an entire query fragment, the database interprets it as part of the query, potentially returning data the user shouldn’t see, modifying data, or even deleting collections.
What problem exists
- Untrusted input is injected into a query structure.
- The database engine executes the injected fragment as a real query clause.
Why the problem matters
- Data confidentiality: attackers can read arbitrary documents.
- Data integrity: attackers can update or delete data.
- Availability: attackers can trigger expensive queries that exhaust resources.
What happens if this concept is not used
- Sensitive data leaks (e.g., user profiles, financial records).
- Unauthorized data modification or deletion.
- Potential denial‑of‑service by forcing heavy queries.
Where this concept appears in real backend systems
- User authentication endpoints that build MongoDB queries from
req.body. - Search APIs that merge query parameters into a
$matchstage. - Admin dashboards that allow dynamic filtering without sanitization.
2. Mental model
Analogy
Imagine a chef who writes a recipe by copying the customer’s handwritten note directly into the cooking instructions. If the note contains a hidden ingredient or a dangerous command (“add poison”), the chef will unknowingly follow it, ruining the dish. The chef needs a safe way to interpret the note—like a parser that only accepts known ingredients and ignores unknown words.
Engineering model
NoSQL injection is like giving the database a raw recipe that includes the user’s handwritten note. The database engine reads the note as part of the query. To prevent this, we must sanitize or parameterize the input so that only the intended fields are used, and any special characters are treated as data, not as query operators.
3. Deep explanation
Internals
- Query objects: In MongoDB, a query is a JSON object. Operators like
$gt,$ne,$orstart with$. If user input contains these keys, they become operators. - Dynamic query construction: Many ORMs or raw drivers allow building queries by merging objects (
{ ...userInput }). IfuserInputcontains$ne, the resulting query will include that operator. - Injection vector: The attacker supplies a payload such as
{ "$ne": null }or{ "$gt": "" }to bypass authentication or retrieve all documents.
Important terminology
- Injection: Untrusted data is interpreted as part of a query.
- Operator injection: Injecting query operators (
$gt,$or,$where). - Field injection: Injecting field names that the application didn’t intend to expose.
- Sanitization: Removing or escaping dangerous characters/keys.
- Parameterized queries: Using driver APIs that separate data from query structure.
Trade‑offs
| Approach | Pros | Cons |
|---|---|---|
| Strict schema validation | Prevents unknown keys | Requires schema definition; may limit flexibility |
| Whitelist of allowed fields | Simple to implement | Must maintain list; new fields need updates |
| Use of ODM/ORM | Built‑in sanitization | May add overhead; not all ORMs fully protect |
| Manual sanitization | Full control | Error‑prone; hard to maintain |
Failure modes
- False positives: Over‑sanitizing may reject legitimate data.
- False negatives: Missing a new operator in the whitelist.
- Performance hit: Deep validation can slow request handling.
Limitations
- NoSQL injection is mitigated by input validation, not by database configuration alone.
- Some NoSQL engines (e.g., DynamoDB) have stricter query syntax, reducing risk but not eliminating it.
Where this concept fits in a backend architecture
- API layer: Receives raw input; must validate before passing to data layer.
- Service layer: Builds query objects; should use safe APIs.
- Data layer: Executes queries; should not expose raw query construction to callers.
Interaction with related concepts
- Authentication/Authorization: Even if a query is safe, missing auth can expose data.
- Rate limiting: Prevents attackers from flooding with injection attempts.
- Logging: Capture raw queries for audit; ensure logs are sanitized.
4. How it works
- Receive request – User sends JSON payload to
/login. - Validate schema – Use a JSON schema validator to ensure only
usernameandpasswordfields exist. - Sanitize input – Strip any keys starting with
$or containing dots (.). - Build query – Construct a MongoDB query object with only the sanitized fields.
- Execute query – Pass the query to the driver; the database treats it as a literal match.
- Return result – Send back authentication status.
sequenceDiagram
participant Client
participant API
participant Service
participant MongoDB
Client->>API: POST /login { "username": "...", "password": "..." }
API->>API: Validate schema
API->>API: Sanitize input
API->>Service: buildQuery(sanitized)
Service->>MongoDB: findOne(query)
MongoDB-->>Service: document
Service-->>API: authResult
API-->>Client: 200 OK / 401
5. Practical implementation
TypeScript/Node.js example (MongoDB)
import express from 'express';
import { MongoClient, Db } from 'mongodb';
import Ajv from 'ajv';
const app = express();
app.use(express.json());
// 1️⃣ JSON schema for login payload
const loginSchema = {
type: 'object',
properties: {
username: { type: 'string' },
password: { type: 'string' },
},
required: ['username', 'password'],
additionalProperties: false, // ❌ disallow extra keys
};
const ajv = new Ajv();
const validateLogin = ajv.compile(loginSchema);
// 2️⃣ Connect to MongoDB
let db: Db;
MongoClient.connect('mongodb://localhost:27017', { useUnifiedTopology: true })
.then((client) => {
db = client.db('app');
console.log('MongoDB connected');
})
.catch(console.error);
// 3️⃣ Helper: strip dangerous keys
function sanitizeUserInput(input: any) {
const sanitized: any = {};
for (const key in input) {
if (!key.startsWith('