nosql-sanitize is a lightweight and blazing-fast suite of tools designed to prevent NoSQL Injection attacks in Node.js applications. It recursively sanitizes user-supplied data to remove characters and patterns (like $ and control characters) used in malicious MongoDB queries.
- Blazing Fast: Optimized core engine with pre-compiled regex and minimal overhead.
- Recursive Sanitization: Automatically cleans nested objects and arrays.
- Framework Support: First-class support for Express (4.x/5.x) and Fastify (4.x/5.x).
- Prototype Pollution Protection: Blocks
__proto__,constructor, andprototypekeys by default. - Configurable Depth Limiting: Control what happens to values beyond
maxDepthβ preserve, remove, or throw. - Highly Configurable: Control depth, allowed/denied keys, content-types, and more.
- TypeScript Ready: Built-in, high-quality type definitions.
- Security First: Preserves sensitive data like email addresses while stripping injection vectors.
- Packages
- Why?
- Quick Start
- Compatibility
- Configuration Options
- Security
- Advanced Usage
- Performance
- Architecture
- Development & Testing
- Contributing
- License
| Package | Purpose | Installation |
|---|---|---|
@exortek/express-mongo-sanitize |
Express Middleware | npm i @exortek/express-mongo-sanitize |
@exortek/fastify-mongo-sanitize |
Fastify Plugin | npm i @exortek/fastify-mongo-sanitize |
@exortek/nosql-sanitize-core |
Core Engine | npm i @exortek/nosql-sanitize-core |
Every framework major below is exercised by the integration test suite (yarn test:servers), which boots a real server on each and asserts identical sanitization behaviour.
| Runtime | Supported | Verified in CI |
|---|---|---|
| Express | 4.x and 5.x |
β Express 4 + Express 5 servers |
| Fastify | 4.x and 5.x |
β Fastify 4 + Fastify 5 servers |
| Node.js | >= 18 |
β |
| Modules | CommonJS + ESM | require() and import both work |
| TypeScript | Bundled .d.ts |
No @types/* needed |
In MongoDB, operators like $gt, $ne, and $where can be injected via JSON input to bypass logic or extract data:
// β Malicious login attempt
{ "username": "admin", "password": { "$ne": "" } }Without sanitization, this query might return the first user in the database (usually the admin) because password is "not equal to empty string". nosql-sanitize transforms this into:
// β
Sanitized input
{ "username": "admin", "password": { "ne": "" } }const express = require('express');
const mongoSanitize = require('@exortek/express-mongo-sanitize');
const app = express();
app.use(express.json());
app.use(mongoSanitize());
app.post('/login', (req, res) => {
// req.body is already sanitized
res.send('Safe!');
});const fastify = require('fastify')();
const mongoSanitize = require('@exortek/fastify-mongo-sanitize');
fastify.register(mongoSanitize);
fastify.post('/login', async (request) => {
// request.body is already sanitized
return { status: 'Safe!' };
});All packages (express, fastify) accept the same configuration options, which are passed to the core engine.
| Option | Type | Default | Description |
|---|---|---|---|
replaceWith |
string |
'' |
String to replace matched patterns (like $) with. |
removeMatches |
boolean |
false |
If true, removes the entire key-value pair if a match is found. |
sanitizeObjects |
string[] |
['body', 'query'] |
Fields on the request object to sanitize. |
contentTypes |
string[] | null |
['application/json', 'application/x-www-form-urlencoded'] |
Only sanitize body for these content types. null = all. |
skipRoutes |
(string | RegExp)[] |
[] |
Routes to ignore during auto-sanitization. |
recursive |
boolean |
true |
Whether to recursively sanitize nested objects/arrays. |
maxDepth |
number | null |
null |
Maximum recursion depth. 0 = top-level strings only. null = unlimited. |
maxDepthBehavior |
string |
'preserve' |
Action when maxDepth exceeded. See Depth Limiting. |
preserveEmails |
boolean |
true |
Preserve email-looking values. See Email Preservation. |
allowPrototypeKeys |
boolean |
false |
Allow __proto__ / constructor / prototype keys. |
allowedKeys |
string[] |
[] |
Keys to allow without sanitization (e.g., ['$set']). |
deniedKeys |
string[] |
[] |
Keys to completely remove from the input. |
removeEmpty |
boolean |
false |
Remove keys with empty/falsy values after sanitization. |
patterns |
RegExp[] |
[/\$/g, /[\x00-\x1F\x7F-\x9F]/g] |
Regex patterns to match. Override to add custom patterns. |
customSanitizer |
function | null |
null |
Replace the built-in sanitizer with your own function. |
mode |
'auto' | 'manual' |
'auto' |
'auto' sanitizes on every request. 'manual' exposes req.sanitize(). |
onSanitize |
function | null |
null |
Audit callback. See onSanitize Events. |
debug.enabled |
boolean |
false |
Enable detailed logging for debugging. |
Fine-grained control over how string values are sanitized:
app.use(mongoSanitize({
stringOptions: {
trim: true, // Trim whitespace from sanitized strings
lowercase: true, // Convert sanitized strings to lowercase
maxLength: 500, // Truncate values longer than 500 characters (keys are not truncated)
}
}));Control how arrays are post-processed after sanitization:
app.use(mongoSanitize({
arrayOptions: {
filterNull: true, // Remove falsy values (null, undefined, 0, '', false) from arrays
distinct: true, // Remove duplicate values from arrays
}
}));The onSanitize callback fires for every value change, key rename, and removal during sanitization. It provides full visibility into what was changed and why:
app.use(mongoSanitize({
onSanitize: (event) => {
console.log(`[${event.type}] ${event.path}: ${event.key} β ${event.sanitizedKey}`);
}
}));Event shape:
| Field | Description |
|---|---|
type |
'value', 'key', 'both', or 'removed' |
reason |
Only for 'removed': 'deniedKey', 'notAllowed', 'prototypePollution', 'removeMatches', 'removeEmpty', 'maxDepth' |
key |
Original key (e.g., '$gt') |
sanitizedKey |
Key after sanitization (e.g., 'gt') |
path |
Full dotted path (e.g., 'body.user.$gt') |
originalValue |
Value before sanitization |
sanitizedValue |
Value after sanitization (undefined for removals) |
Note: onSanitize adds overhead. If you don't need audit logging, leave it null for maximum performance.
By default, __proto__, constructor, and prototype keys are stripped from all objects. This prevents attacks like:
{ "__proto__": { "isAdmin": true } }Smuggled variants are also caught β for example, a key containing hidden control characters can collapse to __proto__ after sanitization and is then blocked.
To opt out (only if you really know what you're doing):
app.use(mongoSanitize({ allowPrototypeKeys: true }));The maxDepth option limits how deep the sanitizer recurses into nested objects. This is important for two reasons:
- Performance: Deeply nested payloads can cause excessive CPU usage.
- Security: Attackers can craft deeply nested objects to slow down your server (DoS).
The maxDepthBehavior option controls what happens when a value exceeds the depth limit:
| Behavior | Description | Risk |
|---|---|---|
'preserve' (default) |
Pass through the over-depth value unsanitized | Unsafe β injection vectors in deep nesting bypass sanitization. Default for v2 backward compatibility only. |
'remove' (recommended) |
Drop the over-depth value entirely | Safe β fail-closed. Over-depth data is discarded. |
'throw' |
Throw a NoSQLSanitizeError |
Safe β rejects the entire request. Use with error handling middleware. |
Warning:
maxDepthBehavior: 'preserve'is the default for backward compatibility, but it is not secure. An attacker can place injection operators deeper thanmaxDepthand they will pass through unsanitized.
// Recommended production configuration
app.use(mongoSanitize({
maxDepth: 5,
maxDepthBehavior: 'remove',
}));Setting maxDepth: 0 is valid and means "only sanitize top-level string values" β nested objects/arrays are handled per maxDepthBehavior.
By default, values that look like email addresses (e.g., user@example.com) are passed through without sanitization. This prevents the @ or other characters from being accidentally stripped.
Important: When
preserveEmails: true, the email bypass also skips any custompatternsyou provide. If you add custom patterns that must apply to email values, setpreserveEmails: false.
// Custom patterns that must also apply to emails
app.use(mongoSanitize({
preserveEmails: false,
patterns: [/\$/g, /@evil\.com/g],
}));Only application/json and application/x-www-form-urlencoded request bodies are sanitized by default. This prevents corrupting binary uploads, multipart forms, or other non-JSON payloads. The query object is always sanitized.
// Add GraphQL support
app.use(mongoSanitize({
contentTypes: ['application/json', 'application/graphql'],
}));
// Sanitize all content types (use with caution)
app.use(mongoSanitize({ contentTypes: null }));The default patterns strip $ (MongoDB operator prefix) and control characters. You can override these with your own:
app.use(mongoSanitize({
patterns: [
/\$/g, // MongoDB operator prefix
/[\x00-\x1F\x7F-\x9F]/g, // Control characters
/\{.*\}/g, // Curly brace expressions
]
}));Express 5 defines req.query as a non-writable getter, which means req.query = sanitized throws a TypeError. This library detects non-writable properties and applies safe assignment strategies, so it works across Express 4.x and 5.x.
// No extra configuration needed β works with Express 4.x and 5.x
app.use(mongoSanitize());Fastify parses query strings into objects with a null prototype. The sanitizer handles these correctly, so Fastify query objects are sanitized just like regular plain objects.
If you need fine-grained control over when sanitization occurs:
// Express
app.use(mongoSanitize({ mode: 'manual' }));
app.post('/sensitive', (req, res) => {
req.sanitize(); // Trigger sanitization manually
req.sanitize({ maxDepth: 2 }); // Or with custom options
res.json(req.body);
});
// Fastify
fastify.register(mongoSanitize, { mode: 'manual' });
fastify.post('/sensitive', async (request) => {
request.sanitize();
return request.body;
});Route parameters (req.params) are not sanitized by default. Use the paramSanitizeHandler to sanitize specific parameters:
const { paramSanitizeHandler } = require('@exortek/express-mongo-sanitize');
app.param('username', paramSanitizeHandler());
app.get('/user/:username', (req, res) => {
// GET /user/$admin β req.params.username is "admin"
res.json({ username: req.params.username });
});app.use(mongoSanitize({
skipRoutes: [
'/api/v1/webhook', // Exact match (O(1) Set lookup)
/^\/public\/.*/, // Regex match
/^\/api\/v[0-9]+\/health/ // Regex with version pattern
]
}));Replace the built-in sanitizer entirely:
app.use(mongoSanitize({
customSanitizer: (data, options) => {
// data is a shallow clone of the request field (body/query/params)
// options is the resolved configuration
// Return the sanitized data
return myCustomSanitize(data);
}
}));// Allow specific MongoDB operators (e.g., for admin APIs)
app.use(mongoSanitize({
allowedKeys: ['$set', '$push', '$pull']
}));
// Deny specific keys (removed from input entirely)
app.use(mongoSanitize({
deniedKeys: ['password_hash', 'internal_id']
}));Instead of removing $, replace it with an underscore:
app.use(mongoSanitize({
replaceWith: '_'
}));
// { "$gt": 5 } -> { "_gt": 5 }Benchmarked on Apple M4 Pro (14 cores), Node.js v24.x with --expose-gc:
| Payload | Throughput | Latency |
|---|---|---|
| Small body (5 fields) | 820K ops/s | 0.001 ms |
| Medium body (20 fields) | 215K ops/s | 0.005 ms |
| Large body (100 fields) | 39K ops/s | 0.025 ms |
| Nested body (3Γ5, ~155 fields) | 39K ops/s | 0.026 ms |
| skipRoute hit | 2.4M ops/s | 0.0004 ms |
| Configuration | Throughput | Overhead |
|---|---|---|
| Baseline (defaults) | 230K ops/s | β |
+ stringOptions (trim+lower+max) |
214K ops/s | ~7% |
+ removeMatches |
329K ops/s | Faster (early exit) |
+ removeEmpty |
229K ops/s | ~0.5% |
+ allowedKeys (5 keys) |
504K ops/s | Faster (fewer keys) |
+ onSanitize callback |
228K ops/s | ~1% |
| Operation | Throughput |
|---|---|
| Clean string (no match) | 36M ops/s |
Dirty string ($ prefix) |
31M ops/s |
| Email (fast-path skip) | 31M ops/s |
isPlainObject check |
124M ops/s |
isEmail check |
36M ops/s |
| Exact skipRoute match (10 routes) | 33M ops/s |
Run benchmarks locally:
yarn benchornode --expose-gc benchmarks/index.js --save
The project is structured as a monorepo for maximum consistency:
core/: The brain. Contains the logic, default patterns, and options resolver.express/&fastify/: Thin, framework-specific wrappers that adapt the core to each ecosystem.
# Clone and install
git clone https://github.com/ExorTek/nosql-sanitize.git
cd nosql-sanitize
yarn install
# Run tests
yarn test # All suites
yarn test:core # Core only
yarn test:express # Express integration
yarn test:fastify # Fastify integration
# Benchmark
yarn bench # Console output only
yarn bench:save # Save JSON + Markdown report
yarn bench:compare # Save and compare with last run
node --expose-gc benchmarks/index.js # Most accurate (with GC)Contributions are welcome. See CONTRIBUTING.md for the setup, branch/commit conventions, and the yarn verify gate CI runs on every PR. In short:
yarn install
yarn verify # format:check + unit tests + Express/Fastify 4+5 integration serversFor the supported-version matrix, private vulnerability reporting, and the sanitizer's hardening guarantees, see SECURITY.md. Please do not open a public issue for a security bug.
MIT β Created by ExorTek
