Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

46 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

nosql-sanitize logo

express version fastify version

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.

Key Features

  • 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, and prototype keys 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.

πŸ“‘ Contents

πŸ“¦ Packages

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

βœ… Compatibility

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

πŸ€” Why?

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": "" } }

⚑ Quick Start

Express

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!');
});

Fastify

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!' };
});

βš™οΈ Configuration Options

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.

String Options

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)
  }
}));

Array Options

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
  }
}));

onSanitize Events

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.

πŸ”’ Security

Prototype Pollution Protection

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 }));

Depth Limiting

The maxDepth option limits how deep the sanitizer recurses into nested objects. This is important for two reasons:

  1. Performance: Deeply nested payloads can cause excessive CPU usage.
  2. 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 than maxDepth and 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.

Email Preservation

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 custom patterns you provide. If you add custom patterns that must apply to email values, set preserveEmails: false.

// Custom patterns that must also apply to emails
app.use(mongoSanitize({
  preserveEmails: false,
  patterns: [/\$/g, /@evil\.com/g],
}));

Content-Type Guard

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 }));

Custom Patterns

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 Compatibility

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 Null-Proto Query Objects

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.

πŸ›  Advanced Usage

Manual Mode

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 Parameter Sanitization (Express only)

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 });
});

Skipping Specific Routes

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
  ]
}));

Custom Sanitizer

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);
  }
}));

Allowing / Denying Specific Keys

// 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']
}));

Custom Replacement

Instead of removing $, replace it with an underscore:

app.use(mongoSanitize({
  replaceWith: '_'
}));
// { "$gt": 5 } -> { "_gt": 5 }

⚑ Performance

Benchmarked on Apple M4 Pro (14 cores), Node.js v24.x with --expose-gc:

Request Pipeline (Full Sanitization)

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

Feature Overhead (20-field object)

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%

Core Operations

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 bench or node --expose-gc benchmarks/index.js --save

πŸ›  Architecture

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.

πŸ§ͺ Development & Testing

# 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)

🀝 Contributing

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 servers

πŸ›‘ Security

For 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.

πŸ“œ License

MIT β€” Created by ExorTek

About

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.

Topics

Resources

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages