NoSQL Injection
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep and psalm — both specialist static analysis tools that must be configured to flag MongoDB collection methods called with unsanitised $_POST/$_GET input. This is not caught by a default linter or compiler; it requires intentional SAST tooling, but it is automatable once set up.
Closest to 'simple parameterised fix' (e3). The quick_fix is to type-cast every user-supplied value to its expected scalar type before use and reject inputs with '$'-prefixed keys. This is a small, localised change — a validation/casting layer at query-construction points — but it may touch several endpoints/query-building locations without being a full cross-cutting refactor.
Closest to 'localised tax' (b3). The burden falls specifically on code paths that construct MongoDB/Redis queries from user input (web, cli, queue-worker contexts). It imposes a persistent review and discipline requirement on query-building code, but the rest of the codebase is largely unaffected. It does not reshape the entire system architecture.
Closest to 'serious trap' (t7). The misconception field captures this precisely: developers confidently believe NoSQL databases are injection-immune because there is no SQL syntax. This contradicts how injection is understood in SQL contexts and leads developers to skip defences entirely. The operator injection vector ($ne, $gt, $where) is non-obvious and contradicts the 'no SQL, no injection' mental model most developers carry, making this a serious cognitive trap.
Also Known As
TL;DR
Explanation
NoSQL injection is the document-store equivalent of SQL injection, but with attack vectors specific to each engine. In MongoDB, the most common patterns are: operator injection where attacker input becomes a query operator object (e.g. submitting `{"$ne": null}` as a password to bypass a `find({user, password})` query); JavaScript injection via the `$where` operator or `mapReduce`, which executes attacker-controlled JS server-side; and array/object payload injection in fields the application expects to be strings. Redis vulnerabilities cluster around `EVAL`, `SCRIPT LOAD`, and command injection through unvalidated arguments to Lua scripts. Couchbase and other engines have N1QL injection that closely mirrors SQL injection. The unifying defence is the same as SQL: never concatenate untrusted input into queries; use parameterised query builders, type-cast inputs to expected scalar types before query construction, and validate that JSON inputs match a strict schema. PHP's MongoDB driver (`mongodb/mongodb`) accepts BSON arrays — type-casting `$_POST` values to strings before passing them to a query is a critical defensive step.
How It's Exploited
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Passing `$_POST` arrays straight into MongoDB queries — PHP arrays serialize to BSON objects, turning user input into operator structure.
- Using `$where` with concatenated user input — equivalent to executing attacker-controlled JavaScript server-side.
- Assuming JSON parsing sanitises input — `json_decode` happily produces nested arrays that become query operators.
- Treating Redis as injection-free — `EVAL` with concatenated arguments, or commands constructed from user input, are RCE-class vulnerabilities.
- Filtering only string-keys without filtering values — operator injection happens through values that contain keys starting with `$`.
Avoid When
- Code uses a typed query-builder with parameterised values and no raw arrays — validate that's actually the case before assuming safety.
When To Use
- Reviewing any PHP code that builds MongoDB or Redis queries from request input.
- Auditing authentication, search, and update endpoints in document-store applications.
- Setting up CI rules to catch direct $_POST/$_GET use in query construction.
Code Examples
// ❌ MongoDB query built from raw $_POST — accepts operator injection
$user = $collection->findOne([
'username' => $_POST['username'],
'password' => $_POST['password']
]);
// Attacker submits password[$ne]=x — matches any non-'x' password.
// Authentication bypass is one form post away.
// ✅ Cast to scalar string and validate — operator structure cannot survive
$username = (string) ($_POST['username'] ?? '');
$password = (string) ($_POST['password'] ?? '');
if ($username === '' || $password === '' || strlen($password) > 1024) {
throw new InvalidArgumentException('Invalid credentials format');
}
$user = $collection->findOne([
'username' => $username,
'password' => password_verify_lookup($password)
]);
// For arbitrary user input that must be JSON, validate against a schema
// (e.g. opis/json-schema) before passing fields to the query builder.