Topic: mcp server nosql injection security
MCP server NoSQL injection security — MongoDB $where, regex, and $ne bypass in MCP tool handlers
NoSQL databases do not use SQL, but they are not injection-safe. MongoDB-backed MCP server tool handlers that pass JSON arguments directly into query objects are vulnerable to operator injection: an attacker — or an LLM following an adversarial instruction — can replace a string field with a MongoDB operator object, turning a simple equality check into a JavaScript evaluation, a regex enumeration, or an authentication bypass. The attack surface is the JSON type system itself.
Why NoSQL injection is a JSON type problem
SQL injection works by breaking out of a string quote. NoSQL injection against MongoDB works by substituting an unexpected JSON type. A MongoDB query filter expects {"username": "alice"} — a string value. If an attacker can pass {"username": {"$ne": null}} — an object value with a MongoDB operator — the database interprets the operator semantics and returns all documents where username is not null (i.e., all users).
MCP tool arguments are JSON. They arrive at the handler as parsed JavaScript or Python objects. If the handler passes those objects into MongoDB queries without type-checking the field values, the database receives attacker-controlled operator objects.
Attack 1 — $ne authentication bypass
The classic NoSQL authentication bypass uses $ne (not-equal) to bypass credential checks:
// Vulnerable MCP tool handler — login_user
async function login_user({ username, password }) {
// username and password come from LLM tool arguments
const user = await db.collection('users').findOne({
username: username,
password: password // ← passed directly without type check
});
if (user) return { token: generateToken(user._id) };
return { error: 'Invalid credentials' };
}
// Injected arguments:
// { "username": "admin", "password": { "$ne": null } }
// Resulting query: { username: "admin", password: { $ne: null } }
// Matches the admin user because their password is not null — bypass achieved
The fix requires validating that password is a string before it reaches the query. An object value should be rejected immediately:
async function login_user({ username, password }) {
if (typeof username !== 'string' || typeof password !== 'string') {
throw new Error('username and password must be strings');
}
// Now safe to use in query — both are guaranteed primitive strings
const user = await db.collection('users').findOne({
username: username,
passwordHash: await bcrypt.hash(password, 10) // hash before querying
});
// ...
}
Attack 2 — $regex enumeration
When a search tool accepts a query string and inserts it into a MongoDB string match, replacing the string with a $regex operator enables systematic enumeration of field values:
// Vulnerable — search_users tool
async function search_users({ query }) {
return db.collection('users').find({
email: query // ← if query = { "$regex": "^a", "$options": "i" }, returns all emails starting with 'a'
}).toArray();
}
// Enumeration attack sequence:
// { "query": { "$regex": "^a" } } → returns users with email starting with 'a'
// { "query": { "$regex": "^b" } } → returns users with email starting with 'b'
// ... 26 calls enumerate the first character of every email in the database
// { "query": { "$regex": "^alice@" } } → confirms alice@... exists
// Binary-search on characters: 7 calls per character → full email harvesting
This attack is particularly dangerous in MCP contexts because an LLM agent can execute the enumeration loop autonomously, making hundreds of tool calls in a single session to systematically extract all user records.
// Fix — reject non-string search values; escape regex metacharacters
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function search_users({ query }) {
if (typeof query !== 'string') throw new Error('query must be a string');
if (query.length > 200) throw new Error('query too long');
// Use a plain string match or a constrained, escaped prefix search
return db.collection('users').find({
email: { $regex: `^${escapeRegex(query)}`, $options: 'i' }
}).limit(20).toArray(); // Always limit results
}
Attack 3 — $where JavaScript execution
MongoDB's $where operator allows a JavaScript expression or function to filter documents server-side. If an attacker can inject a $where value, they can execute arbitrary JavaScript in the MongoDB shell process:
// Vulnerable — filter_records tool
async function filter_records({ filter }) {
// filter comes from LLM argument, passed directly to MongoDB
return db.collection('records').find(filter).toArray();
}
// Injected:
// { "filter": { "$where": "function() { return sleep(5000) || true; }" } }
// → denial of service via per-document sleep
// More dangerous:
// { "filter": { "$where": "function() { db.dropDatabase(); return true; }" } }
// → destructive operation (if the MongoDB user has dropDatabase permission)
The fix for $where injection has two components: disable JavaScript execution in MongoDB entirely, and never pass raw tool argument objects as MongoDB query filters:
// MongoDB connection — disable server-side JavaScript
const client = new MongoClient(uri, {
// Disable $where, $function, and mapReduce (all require server-side JS)
// This is a connection-level defense — also set in mongod.conf:
// security.javascriptEnabled: false
});
// In mongod.conf / replica set config:
// security:
// javascriptEnabled: false
// Application-level: never use filter objects from tool arguments directly
// Build the query from validated, typed fields only
async function filter_records({ status, created_after, limit = 20 }) {
const validStatuses = ['active', 'archived', 'pending'];
if (!validStatuses.includes(status)) throw new Error('Invalid status');
if (typeof created_after !== 'string') throw new Error('created_after must be an ISO date string');
const date = new Date(created_after);
if (isNaN(date)) throw new Error('created_after must be a valid ISO date');
const safeLimit = Math.min(Math.max(1, Number(limit)), 100);
return db.collection('records').find({
status,
createdAt: { $gte: date }
}).limit(safeLimit).toArray();
}
Structural prevention: never spread tool arguments into query objects
The root cause of all three attacks is spreading or directly using LLM-provided arguments as MongoDB filter objects. The architectural fix is to construct query objects from validated, typed fields only — never from free-form tool argument objects:
// Anti-pattern — operator injection surface
async function query_tool(args) {
return collection.find(args).toArray(); // ← never do this
}
// Safe pattern — explicit field extraction and type validation
async function query_tool({ name, status, page = 1 }) {
if (typeof name !== 'string') throw new Error('name must be string');
if (!['active', 'inactive'].includes(status)) throw new Error('invalid status');
const skip = (Math.max(1, Number(page)) - 1) * 20;
return collection.find({
name: { $regex: `^${escapeRegex(name)}`, $options: 'i' },
status // plain string equality — no operator injection possible
}).skip(skip).limit(20).toArray();
}
Mongoose schema enforcement as a secondary layer
Mongoose schemas enforce types at the ODM layer, rejecting object values for fields declared as String. This provides a secondary defense against $ne and $regex injection when used correctly:
const UserSchema = new mongoose.Schema({
username: { type: String, required: true },
email: { type: String, required: true },
passwordHash: { type: String, required: true }
});
// Mongoose will cast { $ne: null } to the string "[object Object]" for String fields
// This breaks the injection (the query no longer matches) without throwing
// But do not rely on this alone — validate types explicitly before calling findOne
SkillAudit checks for NoSQL injection risk
SkillAudit's static analysis checks for MongoDB query construction patterns that spread tool arguments into filter objects, missing type guards before MongoDB calls, and MongoDB connection configurations that enable server-side JavaScript. An A-grade MCP server constructs all MongoDB query filters from validated, typed fields and disables $where at the database level. See the MCP server security checklist for the full injection gate.
Check your MongoDB-backed MCP server for injection risks
SkillAudit checks for operator injection patterns, $where usage, and missing type validation in MCP tool handlers in 60 seconds.
Run a free audit