@exortek/express-mongo-sanitize 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,269 +1,98 @@
1
1
  # @exortek/express-mongo-sanitize
2
2
 
3
- A comprehensive Express middleware designed to protect your No(n)SQL queries from injection attacks by sanitizing request data.
4
- This middleware provides flexible sanitization options for request bodies and query strings, and an **optional handler for route parameters**.
3
+ <p align="center">
4
+ <img src="https://img.shields.io/npm/v/@exortek/express-mongo-sanitize?style=flat-square&color=339933" alt="npm version">
5
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
6
+ </p>
5
7
 
6
- ## Compatibility
8
+ Express middleware for NoSQL injection prevention. Sanitizes request `body`, `query`, and `params` to protect MongoDB queries from operator injection attacks.
7
9
 
8
- | Middleware version | Express version |
9
- |--------------------|:---------------:|
10
- | `^1.x` | `^4.x` |
11
- | `^1.x` | `^5.x` |
10
+ ## 📦 Installation
12
11
 
13
- ## Features
14
-
15
- - Automatic sanitization of `req.body` and `req.query` by default
16
- - Supports deep/nested objects, arrays, and string transformation
17
- - Allows custom sanitizer logic, key allow/deny lists, skip routes, and more
18
- - **Route params (`req.params`) can be sanitized with an explicit helper** (see below)
19
-
20
- ---
21
-
22
- ## Installation
23
-
24
- ```sh
25
- yarn add @exortek/express-mongo-sanitize
26
- # or
12
+ ```bash
27
13
  npm install @exortek/express-mongo-sanitize
28
14
  ```
15
+ ```bash
16
+ yarn install @exortek/express-mongo-sanitize
17
+ ```
18
+ ```bash
19
+ pnpm install @exortek/express-mongo-sanitize
20
+ ```
29
21
 
30
- ---
31
-
32
- ## Usage
33
-
34
- ### Basic usage
22
+ ## ⚡ Quick Start
35
23
 
36
24
  ```js
37
25
  const express = require('express');
38
- const expressMongoSanitize = require('@exortek/express-mongo-sanitize');
26
+ const mongoSanitize = require('@exortek/express-mongo-sanitize');
39
27
 
40
28
  const app = express();
41
29
  app.use(express.json());
30
+ app.use(mongoSanitize());
42
31
 
43
- // Body and query are sanitized automatically:
44
- app.use(expressMongoSanitize());
45
-
46
- app.post('/submit', (req, res) => {
32
+ app.post('/login', (req, res) => {
33
+ // req.body is sanitized — { "$ne": "" } becomes { "ne": "" }
47
34
  res.json(req.body);
48
35
  });
49
36
  ```
50
37
 
51
- ---
52
-
53
- ### Sanitizing Route Params (`req.params`)
54
-
55
- By default, only `body` and `query` are sanitized.
56
- **If you want to sanitize route parameters (`req.params`),**
57
- use the exported `paramSanitizeHandler` with Express's `app.param` or `router.param`:
38
+ ## ⚙️ Options
58
39
 
59
40
  ```js
60
- // Route parameter sanitization (recommended way):
61
- app.param('username', expressMongoSanitize.paramSanitizeHandler());
62
-
63
- // Example route:
64
- app.get('/user/:username', (req, res) => {
65
- res.json({ username: req.params.username });
66
- });
41
+ app.use(mongoSanitize({
42
+ replaceWith: '', // Replace matched chars with this string
43
+ removeMatches: false, // Remove entire key-value pair if pattern matches
44
+ sanitizeObjects: ['body', 'query'], // Request fields to sanitize
45
+ contentTypes: ['application/json', 'application/x-www-form-urlencoded'],
46
+ mode: 'auto', // 'auto' | 'manual'
47
+ skipRoutes: [], // Routes to skip (string or RegExp)
48
+ recursive: true, // Sanitize nested objects
49
+ maxDepth: null, // Max recursion depth (null = unlimited)
50
+ onSanitize: ({ key, originalValue, sanitizedValue }) => {
51
+ console.log(`Sanitized ${key}`);
52
+ }
53
+ }));
67
54
  ```
68
55
 
69
- **Note:**
70
- - You can attach this for any route param, e.g. `'id'`, `'slug'`, etc.
71
- - This gives you full control and doesn't require the middleware to know your routes.
72
-
73
- ---
56
+ > For the full list of options, see the [Core README](../core/README.md#configuration-options).
74
57
 
75
- ## Options
58
+ ## 🛠 Features
76
59
 
77
- | Option | Type | Default | Description |
78
- |-------------------|----------|-------------------------------------|---------------------------------------------------------------------|
79
- | `replaceWith` | string | `''` | String to replace matched patterns |
80
- | `removeMatches` | boolean | `false` | Remove values matching patterns entirely |
81
- | `sanitizeObjects` | string[] | `['body', 'query']` | List of request objects to sanitize |
82
- | `mode` | string | `'auto'` | `'auto'` for automatic, `'manual'` for explicit req.sanitize() call |
83
- | `skipRoutes` | string[] | `[]` | List of paths to skip (e.g. ['/health']) |
84
- | `customSanitizer` | function | `null` | Custom sanitizer function, overrides built-in sanitizer |
85
- | `recursive` | boolean | `true` | Recursively sanitize nested values |
86
- | `removeEmpty` | boolean | `false` | Remove empty values after sanitization |
87
- | `patterns` | RegExp[] | See source code | Patterns to match for sanitization |
88
- | `allowedKeys` | string[] | `[]` | Only allow these keys (all if empty) |
89
- | `deniedKeys` | string[] | `[]` | Remove these keys (none if empty) |
90
- | `stringOptions` | object | See below | String transform options (trim, lowercase, maxLength) |
91
- | `arrayOptions` | object | See below | Array handling options (filterNull, distinct) |
92
- | `debug` | object | `{ enabled: false, level: "info" }` | Enables debug logging for middleware internals. |
60
+ ### Route Parameter Sanitization
93
61
 
94
-
95
- #### `stringOptions` default:
96
-
97
- ```js
98
- {
99
- trim: false,
100
- lowercase: false,
101
- maxLength: null
102
- }
103
- ```
104
-
105
- #### `arrayOptions` default:
62
+ While `body` and `query` are sanitized automatically, route parameters can be sanitized using the `paramSanitizeHandler`:
106
63
 
107
64
  ```js
108
- {
109
- filterNull: false,
110
- distinct: false
111
- }
112
- ```
113
-
114
- ---
115
-
116
- ## Manual Mode
117
-
118
- If you set `mode: 'manual'`, the middleware will not sanitize automatically.
119
- Call `req.sanitize()` manually in your route:
65
+ const { paramSanitizeHandler } = require('@exortek/express-mongo-sanitize');
120
66
 
121
- ```js
122
- app.use(expressMongoSanitize({ mode: 'manual' }));
67
+ app.param('username', paramSanitizeHandler());
123
68
 
124
- app.post('/manual', (req, res) => {
125
- req.sanitize({ replaceWith: '_' }); // custom options are supported here
126
- res.json(req.body);
69
+ app.get('/user/:username', (req, res) => {
70
+ // GET /user/$admin req.params.username is "admin"
71
+ res.json({ username: req.params.username });
127
72
  });
128
73
  ```
129
74
 
130
- ---
131
-
132
- ## Skipping Routes
133
-
134
- Skip certain routes by adding their paths to `skipRoutes`:
135
-
136
- ```js
137
- app.use(expressMongoSanitize({ skipRoutes: ['/skip', '/status'] }));
138
-
139
- // These routes will NOT be sanitized
140
- ```
141
-
142
- ---
143
-
144
- ## Custom Sanitizer
75
+ ### Manual Mode
145
76
 
146
- Use a completely custom sanitizer function:
77
+ If you need fine-grained control over when sanitization occurs:
147
78
 
148
79
  ```js
149
- app.use(expressMongoSanitize({
150
- customSanitizer: (data, options) => {
151
- // Your custom logic
152
- return data;
153
- }
154
- }));
155
- ```
156
-
157
- ---
158
-
159
- ## Route Parameter Sanitization
160
-
161
- > By default, only `body` and `query` are sanitized.
162
- > If you want to sanitize route parameters (`req.params`),
163
- > use the helper function with `app.param` or `router.param`:
164
- >
165
- > ```js
166
- > app.param('username', expressMongoSanitize.paramSanitizeHandler());
167
- > ```
168
- >
169
- > This ensures that, for example, `/user/$admin` will be returned as `{ username: 'admin' }`
170
- > in your handler.
80
+ app.use(mongoSanitize({ mode: 'manual' }));
171
81
 
172
- ---
173
-
174
- ## TypeScript
175
-
176
- Type definitions are included.
177
- You can use this plugin in both CommonJS and ESM projects.
178
-
179
- ---
180
-
181
- ## Advanced Usage
182
-
183
- ### Custom Sanitizer per Route
184
-
185
- You can override sanitizer options or use a completely custom sanitizer per route:
186
-
187
- ```js
188
- app.post('/profile', (req, res, next) => {
189
- req.sanitize({
190
- customSanitizer: (data) => {
191
- // For example, redact all strings:
192
- if (typeof data === 'string') return '[REDACTED]';
193
- return data;
194
- }
195
- });
82
+ app.post('/sensitive', (req, res) => {
83
+ req.sanitize(); // Manually trigger sanitization
196
84
  res.json(req.body);
197
85
  });
198
86
  ```
199
- ## Debugging & Logging
200
-
201
- You can enable debug logs to see the internal operation of the middleware.
202
- Useful for troubleshooting or when tuning sanitization behavior.
203
-
204
- ```js
205
- app.use(
206
- expressMongoSanitize({
207
- debug: {
208
- enabled: true, // Turn on debug logs
209
- level: 'debug', // Log level: 'error' | 'warn' | 'info' | 'debug' | 'trace'
210
- logSkippedRoutes: true // (optional) Log when routes are skipped
211
- },
212
- // ...other options
213
- })
214
- );
215
- ```
216
- ### Logging Levels
217
- | Level | Description |
218
- |---------|--------------------------------------|
219
- | `error` | Logs only errors |
220
- | `warn` | Logs warnings and errors |
221
- | `info` | Logs informational messages |
222
- | `debug` | Logs detailed debug information |
223
- | `trace` | Logs very detailed trace information |
224
-
225
- ### Using with Router
226
-
227
- ```js
228
- const router = express.Router();
229
- router.use(expressMongoSanitize());
230
- // You can use paramSanitizeHandler on router params as well:
231
- router.param('userId', expressMongoSanitize.paramSanitizeHandler());
232
- ```
233
-
234
- ---
235
-
236
- ## Troubleshooting
237
87
 
238
- ### Route parameters are not being sanitized
88
+ ### Content-Type Guard
239
89
 
240
- By default, only `body` and `query` are sanitized.
241
- To sanitize route parameters, use:
90
+ By default, only `application/json` and `application/x-www-form-urlencoded` bodies are sanitized to avoid corrupting binary data or file uploads. You can customize this:
242
91
 
243
92
  ```js
244
- app.param('username', expressMongoSanitize.paramSanitizeHandler());
93
+ app.use(mongoSanitize({ contentTypes: ['application/json', 'application/graphql'] }));
245
94
  ```
246
95
 
247
- ### Skipping specific routes doesn't work as expected
248
-
249
- Make sure you use the exact path as in your route definition,
250
- and that you apply the middleware before your routes.
251
-
252
- ---
253
-
254
- ### My request is not being sanitized
255
-
256
- - Ensure your route handler is after the middleware in the stack.
257
- - If you are using `mode: 'manual'`, you **must** call `req.sanitize()` yourself.
258
-
259
- ---
260
-
261
- For more troubleshooting, open an issue at [Github Issues](https://github.com/ExorTek/express-mongo-sanitize/issues)
262
-
263
- ---
264
-
265
- ## License
266
-
267
- **[MIT](https://github.com/ExorTek/express-mongo-sanitize/blob/master/LICENSE)**<br>
96
+ ## 📜 License
268
97
 
269
- Copyright © 2025 ExorTek
98
+ [MIT](../../LICENSE) Created by **ExorTek**
package/package.json CHANGED
@@ -1,56 +1,67 @@
1
1
  {
2
2
  "name": "@exortek/express-mongo-sanitize",
3
- "version": "1.1.1",
4
- "description": "A comprehensive Express middleware designed to protect your No(n)SQL queries from injection attacks by sanitizing request data. This middleware provides flexible sanitization options for request bodies, parameters, and query strings.",
5
- "main": "index.js",
3
+ "version": "2.0.0",
4
+ "description": "Express middleware for NoSQL injection prevention sanitizes request data",
5
+ "main": "src/index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./src/index.js",
11
+ "types": "./types/index.d.ts"
12
+ }
13
+ },
8
14
  "scripts": {
9
- "format": "prettier --write \"**/*.{js,ts,json}\"",
10
- "test:tsd": "tsd",
11
- "test:node": "node --test"
15
+ "test": "node --test test/*.test.js",
16
+ "prepublishOnly": "npm test"
12
17
  },
13
18
  "repository": {
14
19
  "type": "git",
15
- "url": "git+https://github.com/ExorTek/express-mongo-sanitize.git"
20
+ "url": "git+https://github.com/ExorTek/nosql-sanitize.git",
21
+ "directory": "packages/express"
22
+ },
23
+ "homepage": "https://github.com/ExorTek/nosql-sanitize/tree/main/packages/express#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/ExorTek/nosql-sanitize/issues"
16
26
  },
17
27
  "keywords": [
18
28
  "express",
19
29
  "mongodb",
20
30
  "sanitize",
21
- "mongoose",
22
- "express-middleware",
23
- "data-validation",
24
- "input-sanitization",
31
+ "nosql",
25
32
  "security",
26
- "web-application",
27
- "nodejs",
28
- "typescript",
29
33
  "middleware",
30
- "api-security",
31
- "query-sanitization",
32
- "no-sql",
33
- "mongodb-sanitizer"
34
+ "injection",
35
+ "backend"
34
36
  ],
35
37
  "author": "ExorTek - https://github.com/ExorTek",
36
38
  "license": "MIT",
37
- "bugs": {
38
- "url": "https://github.com/ExorTek/express-mongo-sanitize/issues"
39
+ "engines": {
40
+ "node": ">=18.0.0"
39
41
  },
40
- "homepage": "https://github.com/ExorTek/express-mongo-sanitize#readme",
41
42
  "publishConfig": {
42
43
  "access": "public"
43
44
  },
44
- "packageManager": "yarn@4.9.2",
45
+ "dependencies": {
46
+ "@exortek/nosql-sanitize-core": "^2.0.0"
47
+ },
48
+ "peerDependencies": {
49
+ "express": ">=4.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "express": {
53
+ "optional": false
54
+ }
55
+ },
45
56
  "devDependencies": {
46
- "@types/express": "^5.0.3",
57
+ "@types/express": "^5.0.6",
47
58
  "express": "npm:express@5.1.0",
48
- "express4": "npm:express@4.21.2",
49
- "prettier": "^3.5.3",
50
- "tsd": "^0.32.0"
59
+ "express4": "npm:express@4.21.2"
51
60
  },
52
61
  "files": [
53
- "index.js",
54
- "types/index.d.ts"
62
+ "src/",
63
+ "types/",
64
+ "README.md",
65
+ "LICENSE"
55
66
  ]
56
67
  }
package/src/index.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ resolveOptions,
5
+ handleRequest,
6
+ shouldSkipRoute,
7
+ sanitizeString,
8
+ cleanUrl,
9
+ log,
10
+ isString,
11
+ NoSQLSanitizeError,
12
+ } = require('@exortek/nosql-sanitize-core');
13
+
14
+ /**
15
+ * Express middleware factory.
16
+ * @param {Object} [options={}]
17
+ * @returns {Function} Express middleware
18
+ */
19
+ const expressMongoSanitize = (options = {}) => {
20
+ const opts = resolveOptions(options);
21
+
22
+ return (req, res, next) => {
23
+ const requestPath = req.path || req.url;
24
+
25
+ if (shouldSkipRoute(requestPath, opts.skipRoutes, opts.debug)) {
26
+ return next();
27
+ }
28
+
29
+ if (opts.mode === 'auto') {
30
+ handleRequest(req, opts);
31
+ }
32
+
33
+ if (opts.mode === 'manual') {
34
+ req.sanitize = (customOpts) => {
35
+ const finalOpts = customOpts ? resolveOptions({ ...options, ...customOpts }) : opts;
36
+ handleRequest(req, finalOpts);
37
+ };
38
+ }
39
+
40
+ next();
41
+ };
42
+ };
43
+
44
+ /**
45
+ * Route parameter sanitization handler.
46
+ * @param {Object} [options={}]
47
+ * @returns {Function} Express param handler
48
+ */
49
+ const paramSanitizeHandler = (options = {}) => {
50
+ const opts = resolveOptions(options);
51
+
52
+ return function (req, res, next, value, paramName) {
53
+ const key = paramName || this?.name;
54
+ if (key && req.params && isString(value)) {
55
+ req.params[key] = sanitizeString(value, opts, true);
56
+ }
57
+ next();
58
+ };
59
+ };
60
+
61
+ module.exports = expressMongoSanitize;
62
+ module.exports.default = expressMongoSanitize;
63
+ module.exports.expressMongoSanitize = expressMongoSanitize;
64
+ module.exports.paramSanitizeHandler = paramSanitizeHandler;
package/types/index.d.ts CHANGED
@@ -1,80 +1,55 @@
1
- import { RequestHandler, RequestParamHandler } from 'express';
1
+ /// <reference types="node" />
2
2
 
3
- /**
4
- * String-specific sanitizer options.
5
- */
6
- export interface StringOptions {
7
- /** Trim whitespace from strings */
8
- trim?: boolean;
9
- /** Convert strings to lowercase */
10
- lowercase?: boolean;
11
- /** Truncate strings to a maximum length (null = unlimited) */
12
- maxLength?: number | null;
13
- }
3
+ import { Request, Response, NextFunction, RequestHandler } from 'express';
4
+ import { SanitizeOptions, ResolvedOptions, SanitizeEvent } from '@exortek/nosql-sanitize-core';
14
5
 
15
- /**
16
- * Array-specific sanitizer options.
17
- */
18
- export interface ArrayOptions {
19
- /** Remove null values from arrays */
20
- filterNull?: boolean;
21
- /** Remove duplicate values from arrays */
22
- distinct?: boolean;
6
+ declare module 'express' {
7
+ interface Request {
8
+ /**
9
+ * Available when `mode: 'manual'`.
10
+ * Call to sanitize `req.body`, `req.query`, and/or `req.params`.
11
+ * Optionally pass overrides for this specific call.
12
+ */
13
+ sanitize?: (options?: expressMongoSanitize.SanitizeOptions) => void;
14
+ }
23
15
  }
24
16
 
25
- export interface DebugOptions {
26
- /** Enable debug logging */
27
- enabled?: boolean;
28
- /** Log level (e.g., 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace') */
29
- level?: string;
30
- }
17
+ type ExpressMongoSanitize = (options?: expressMongoSanitize.SanitizeOptions) => RequestHandler;
31
18
 
32
- /**
33
- * Main options for expressMongoSanitize middleware.
34
- */
35
- export interface ExpressMongoSanitizeOptions {
36
- /** String to replace matched patterns with */
37
- replaceWith?: string;
38
- /** Remove values matching patterns */
39
- removeMatches?: boolean;
40
- /** Request objects to sanitize (default: ['body', 'query']) */
41
- sanitizeObjects?: Array<'body' | 'query'>;
42
- /** Automatic or manual mode */
43
- mode?: 'auto' | 'manual';
44
- /** Paths to skip sanitizing */
45
- skipRoutes?: string[];
46
- /** Completely custom sanitizer function */
47
- customSanitizer?: (data: any, options: ExpressMongoSanitizeOptions) => any;
48
- /** Recursively sanitize nested objects */
49
- recursive?: boolean;
50
- /** Remove empty values after sanitizing */
51
- removeEmpty?: boolean;
52
- /** Patterns to match for sanitization */
53
- patterns?: RegExp[];
54
- /** Only allow these keys in sanitized objects */
55
- allowedKeys?: string[];
56
- /** Remove these keys from sanitized objects */
57
- deniedKeys?: string[];
58
- /** String sanitizer options */
59
- stringOptions?: StringOptions;
60
- /** Array sanitizer options */
61
- arrayOptions?: ArrayOptions;
62
- /** Debugging options */
63
- debug?: DebugOptions;
64
- }
19
+ declare namespace expressMongoSanitize {
20
+ export { SanitizeOptions, ResolvedOptions, SanitizeEvent };
65
21
 
66
- /**
67
- * Middleware for automatic sanitization of request objects.
68
- */
69
- declare function expressMongoSanitize(options?: ExpressMongoSanitizeOptions): RequestHandler;
22
+ /**
23
+ * Express middleware factory for NoSQL injection prevention.
24
+ *
25
+ * Sanitizes `req.body` and `req.query` by default.
26
+ * Supports `mode: 'auto'` (default) and `mode: 'manual'`.
27
+ *
28
+ * @example
29
+ * ```js
30
+ * const mongoSanitize = require('@exortek/express-mongo-sanitize');
31
+ * app.use(mongoSanitize());
32
+ * ```
33
+ */
34
+ export const expressMongoSanitize: ExpressMongoSanitize;
35
+
36
+ /**
37
+ * Express route parameter sanitization handler.
38
+ *
39
+ * @example
40
+ * ```js
41
+ * const { paramSanitizeHandler } = require('@exortek/express-mongo-sanitize');
42
+ * app.param('userId', paramSanitizeHandler());
43
+ * app.param('slug', paramSanitizeHandler({ replaceWith: '_' }));
44
+ * ```
45
+ */
46
+ export function paramSanitizeHandler(
47
+ options?: SanitizeOptions,
48
+ ): (req: Request, res: Response, next: NextFunction, value: string, paramName: string) => void;
49
+
50
+ export { expressMongoSanitize as default };
51
+ }
70
52
 
71
- /**
72
- * Middleware for sanitizing individual route parameters.
73
- */
74
- declare function paramSanitizeHandler(options?: ExpressMongoSanitizeOptions): RequestParamHandler;
53
+ declare function expressMongoSanitize(...params: Parameters<ExpressMongoSanitize>): ReturnType<ExpressMongoSanitize>;
75
54
 
76
- /**
77
- * Main export for express-mongo-sanitize middleware.
78
- */
79
- export default expressMongoSanitize;
80
- export { expressMongoSanitize, paramSanitizeHandler };
55
+ export = expressMongoSanitize;
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Memet
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/index.js DELETED
@@ -1,479 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Regular expression patterns used for sanitizing input data.
5
- * These patterns match common MongoDB injection attack vectors.
6
- * @constant {ReadonlyArray<RegExp>}
7
- */
8
- const PATTERNS = Object.freeze([
9
- /\$/g,
10
- /\./g,
11
- /[\\\/{}.(*+?|[\]^)]/g,
12
- /[\u0000-\u001F\u007F-\u009F]/g,
13
- /\{\s*\$|\$?\{(.|\r?\n)*\}/g,
14
- ]);
15
-
16
- /**
17
- * Default configuration options for the sanitizer.
18
- * @constant {Object}
19
- * @property {string} replaceWith - String to replace sanitized content with
20
- * @property {boolean} removeMatches - Whether to remove matches entirely
21
- * @property {string[]} sanitizeObjects - Request objects to sanitize
22
- * @property {string} mode - Operation mode ('auto' or 'manual')
23
- * @property {string[]} skipRoutes - Routes to skip sanitization
24
- * @property {Function|null} customSanitizer - Custom sanitization function
25
- * @property {boolean} recursive - Whether to sanitize recursively
26
- * @property {boolean} removeEmpty - Whether to remove empty values
27
- * @property {RegExp[]} patterns - Patterns to match for sanitization
28
- * @property {string[]} allowedKeys - Keys that are allowed
29
- * @property {string[]} deniedKeys - Keys that are denied
30
- * @property {Object} stringOptions - String-specific options
31
- * @property {Object} arrayOptions - Array-specific options
32
- * @property {Object} debug - Debug configuration
33
- */
34
- const DEFAULT_OPTIONS = Object.freeze({
35
- replaceWith: '',
36
- removeMatches: false,
37
- sanitizeObjects: ['body', 'query'],
38
- mode: 'auto',
39
- skipRoutes: [],
40
- customSanitizer: null,
41
- recursive: true,
42
- removeEmpty: false,
43
- patterns: PATTERNS,
44
- allowedKeys: [],
45
- deniedKeys: [],
46
- stringOptions: {
47
- trim: false,
48
- lowercase: false,
49
- maxLength: null,
50
- },
51
- arrayOptions: {
52
- filterNull: false,
53
- distinct: false,
54
- },
55
- debug: {
56
- enabled: false,
57
- level: 'info',
58
- logSkippedRoutes: false,
59
- },
60
- });
61
-
62
- /**
63
- * Log level priority mapping for filtering debug output.
64
- * @constant {Object<string, number>}
65
- */
66
- const LOG_LEVELS = Object.freeze({
67
- silent: 0,
68
- error: 1,
69
- warn: 2,
70
- info: 3,
71
- debug: 4,
72
- trace: 5,
73
- });
74
-
75
- /**
76
- * ANSI color codes for console output formatting.
77
- * @constant {Object<string, string>}
78
- */
79
- const LOG_COLORS = Object.freeze({
80
- error: '\x1b[31m',
81
- warn: '\x1b[33m',
82
- info: '\x1b[36m',
83
- debug: '\x1b[90m',
84
- trace: '\x1b[35m',
85
- reset: '\x1b[0m',
86
- });
87
-
88
- /**
89
- * Logs debug messages with colored output and formatting.
90
- * @param {Object} debugOpts - Debug configuration options
91
- * @param {string} level - Log level (error, warn, info, debug, trace)
92
- * @param {string} context - Context identifier for the log message
93
- * @param {string} message - Main log message
94
- * @param {*} [data=null] - Optional data to include in the log
95
- */
96
- const log = (debugOpts, level, context, message, data = null) => {
97
- if (!debugOpts?.enabled || LOG_LEVELS[debugOpts.level || 'silent'] < LOG_LEVELS[level]) return;
98
- const color = LOG_COLORS[level] || '';
99
- const reset = LOG_COLORS.reset;
100
- const timestamp = new Date().toISOString();
101
- let logMessage = `${color}[mongo-sanitize:${level.toUpperCase()}]${reset} ${timestamp} [${context}] ${message}`;
102
- if (data !== null) {
103
- if (typeof data === 'object') {
104
- console.log(logMessage);
105
- console.log(`${color}Data:${reset}`, JSON.stringify(data, null, 2));
106
- } else {
107
- console.log(logMessage, data);
108
- }
109
- } else {
110
- console.log(logMessage);
111
- }
112
- };
113
-
114
- /**
115
- * Checks if a value is a string.
116
- * @param {*} value - Value to check
117
- * @returns {boolean} True if value is a string
118
- */
119
- const isString = (value) => typeof value === 'string';
120
-
121
- /**
122
- * Validates if a string is a valid email address.
123
- * @param {*} val - Value to validate
124
- * @returns {boolean} True if value is a valid email
125
- */
126
- const isEmail = (val) =>
127
- isString(val) &&
128
- /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i.test(
129
- val
130
- );
131
-
132
- /**
133
- * Checks if a value is a plain object (not array, date, etc.).
134
- * @param {*} obj - Value to check
135
- * @returns {boolean} True if value is a plain object
136
- */
137
- const isPlainObject = (obj) => !!obj && Object.prototype.toString.call(obj) === '[object Object]';
138
-
139
- /**
140
- * Checks if an object is empty (has no own properties).
141
- * @param {*} obj - Object to check
142
- * @returns {boolean} True if object is empty
143
- */
144
- const isObjectEmpty = (obj) => {
145
- if (!isPlainObject(obj)) return false;
146
- return !Object.keys(obj).length;
147
- };
148
-
149
- /**
150
- * Checks if a value is an array.
151
- * @param {*} value - Value to check
152
- * @returns {boolean} True if value is an array
153
- */
154
- const isArray = (value) => Array.isArray(value);
155
-
156
- /**
157
- * Checks if a value is a primitive type (null, boolean, number).
158
- * @param {*} value - Value to check
159
- * @returns {boolean} True if value is primitive
160
- */
161
- const isPrimitive = (value) => value == null || typeof value === 'boolean' || typeof value === 'number';
162
-
163
- /**
164
- * Checks if a value is a Date object.
165
- * @param {*} value - Value to check
166
- * @returns {boolean} True if value is a Date
167
- */
168
- const isDate = (value) => value instanceof Date;
169
-
170
- /**
171
- * Checks if a value is a function.
172
- * @param {*} value - Value to check
173
- * @returns {boolean} True if value is a function
174
- */
175
- const isFunction = (value) => typeof value === 'function';
176
-
177
- /**
178
- * Custom error class for express-mongo-sanitize specific errors.
179
- * @extends Error
180
- */
181
- class ExpressMongoSanitizeError extends Error {
182
- cause;
183
- message;
184
- stack;
185
- /**
186
- * Creates a new ExpressMongoSanitizeError.
187
- * @param {string} message - Error message
188
- * @param {string} [type='generic'] - Error type identifier
189
- */
190
- constructor(message, type = 'generic') {
191
- super(message);
192
- this.name = 'ExpressMongoSanitizeError';
193
- this.type = type;
194
- Error.captureStackTrace(this, this.constructor);
195
- }
196
-
197
- code() {
198
- return this.type;
199
- }
200
-
201
- view() {
202
- return `${this.name} [${this.type}]: ${this.message}\n${this.stack}`;
203
- }
204
- }
205
-
206
- /**
207
- * Sanitizes a string by removing or replacing dangerous patterns.
208
- * @param {string} str - String to sanitize
209
- * @param {Object} options - Sanitization options
210
- * @param {boolean} [isValue=false] - Whether this is a value (affects length limits)
211
- * @returns {string} Sanitized string
212
- */
213
- const sanitizeString = (str, options, isValue = false) => {
214
- const { debug } = options;
215
- if (!isString(str) || isEmail(str)) {
216
- log(debug, 'trace', 'STRING', `Skipping: not a string or is email`, str);
217
- return str;
218
- }
219
- const { replaceWith, patterns, stringOptions } = options;
220
- const combinedPattern = new RegExp(patterns.map((pattern) => pattern.source).join('|'), 'g');
221
- const original = str;
222
- let result = str.replace(combinedPattern, replaceWith);
223
- if (stringOptions.trim) result = result.trim();
224
- if (stringOptions.lowercase) result = result.toLowerCase();
225
- if (stringOptions.maxLength && isValue) result = result.slice(0, stringOptions.maxLength);
226
- if (debug?.enabled && original !== result) {
227
- log(debug, 'debug', 'STRING', `Sanitized string`, { original, result });
228
- }
229
- return result;
230
- };
231
-
232
- /**
233
- * Sanitizes an array by processing each element and applying array-specific options.
234
- * @param {Array} arr - Array to sanitize
235
- * @param {Object} options - Sanitization options
236
- * @returns {Array} Sanitized array
237
- * @throws {ExpressMongoSanitizeError} If input is not an array
238
- */
239
- const sanitizeArray = (arr, options) => {
240
- const { debug } = options;
241
- if (!isArray(arr)) {
242
- log(debug, 'error', 'ARRAY', `Input is not array`, arr);
243
- throw new ExpressMongoSanitizeError('Input must be an array', 'type_error');
244
- }
245
- log(debug, 'trace', 'ARRAY', `Sanitizing array of length ${arr.length}`);
246
- let result = arr.map((item) => sanitizeValue(item, options, true));
247
- if (options.arrayOptions.filterNull) {
248
- const before = result.length;
249
- result = result.filter(Boolean);
250
- log(debug, 'debug', 'ARRAY', `Filtered nulls: ${before} → ${result.length}`);
251
- }
252
- if (options.arrayOptions.distinct) {
253
- const before = result.length;
254
- result = [...new Set(result)];
255
- log(debug, 'debug', 'ARRAY', `Removed duplicates: ${before} → ${result.length}`);
256
- }
257
- return result;
258
- };
259
-
260
- /**
261
- * Sanitizes an object by processing keys and values according to configuration.
262
- * @param {Object} obj - Object to sanitize
263
- * @param {Object} options - Sanitization options
264
- * @returns {Object} Sanitized object
265
- * @throws {ExpressMongoSanitizeError} If input is not an object
266
- */
267
- const sanitizeObject = (obj, options) => {
268
- const { debug, removeEmpty, allowedKeys, deniedKeys, removeMatches, patterns } = options;
269
- if (!isPlainObject(obj)) {
270
- log(debug, 'error', 'OBJECT', `Input is not object`, obj);
271
- throw new ExpressMongoSanitizeError('Input must be an object', 'type_error');
272
- }
273
- log(debug, 'trace', 'OBJECT', `Sanitizing object with keys: ${Object.keys(obj)}`);
274
- return Object.entries(obj).reduce((acc, [key, val]) => {
275
- if ((allowedKeys.size && !allowedKeys.has(key)) || deniedKeys.has(key)) {
276
- log(debug, 'debug', 'OBJECT', `Key '${key}' removed (allowed/denied filter)`);
277
- return acc;
278
- }
279
- const sanitizedKey = sanitizeString(key, options);
280
- if (removeMatches && patterns.some((pattern) => pattern.test(key))) {
281
- log(debug, 'debug', 'OBJECT', `Key '${key}' matches removal pattern`);
282
- return acc;
283
- }
284
- if (removeEmpty && !sanitizedKey) {
285
- log(debug, 'debug', 'OBJECT', `Key '${key}' removed (empty after sanitize)`);
286
- return acc;
287
- }
288
- if (isEmail(val) && deniedKeys.has(key)) {
289
- acc[sanitizedKey] = val;
290
- log(debug, 'trace', 'OBJECT', `Email field preserved: ${key}`);
291
- return acc;
292
- }
293
- if (removeMatches && isString(val) && patterns.some((pattern) => pattern.test(val))) {
294
- log(debug, 'debug', 'OBJECT', `Value for key '${key}' matches removal pattern`);
295
- return acc;
296
- }
297
- const sanitizedValue = sanitizeValue(val, options, true);
298
- if (!removeEmpty || sanitizedValue) acc[sanitizedKey] = sanitizedValue;
299
- return acc;
300
- }, {});
301
- };
302
-
303
- /**
304
- * Main sanitization function that routes values to appropriate sanitizers.
305
- * @param {*} value - Value to sanitize
306
- * @param {Object} options - Sanitization options
307
- * @param {boolean} [isValue=false] - Whether this is a value context
308
- * @returns {*} Sanitized value
309
- */
310
- const sanitizeValue = (value, options, isValue = false) => {
311
- if (!value || isPrimitive(value) || isDate(value)) return value;
312
- if (Array.isArray(value)) return sanitizeArray(value, options);
313
- if (isPlainObject(value)) return sanitizeObject(value, options);
314
- return isString(value) ? sanitizeString(value, options, isValue) : value;
315
- };
316
-
317
- /**
318
- * Validates the provided options object against expected schema.
319
- * @param {Object} options - Options to validate
320
- * @throws {ExpressMongoSanitizeError} If any option is invalid
321
- */
322
- const validateOptions = (options) => {
323
- const validators = {
324
- replaceWith: isString,
325
- removeMatches: isPrimitive,
326
- sanitizeObjects: isArray,
327
- mode: (value) => ['auto', 'manual'].includes(value),
328
- skipRoutes: isArray,
329
- customSanitizer: (value) => value === null || isFunction(value),
330
- recursive: isPrimitive,
331
- removeEmpty: isPrimitive,
332
- patterns: isArray,
333
- allowedKeys: (value) => value === null || isArray(value),
334
- deniedKeys: (value) => value === null || isArray(value),
335
- stringOptions: isPlainObject,
336
- arrayOptions: isPlainObject,
337
- };
338
- for (const [key, validate] of Object.entries(validators)) {
339
- if (!validate(options[key])) {
340
- throw new ExpressMongoSanitizeError(`Invalid configuration: "${key}" with value "${options[key]}"`, 'type_error');
341
- }
342
- }
343
- };
344
-
345
- /**
346
- * Checks if a property is writable on an object or its prototype chain.
347
- * @param {Object} obj - Object to check
348
- * @param {string} prop - Property name to check
349
- * @returns {boolean} True if property is writable
350
- */
351
- const isWritable = (obj, prop) => {
352
- let cur = obj;
353
- while (cur) {
354
- const descriptor = Object.getOwnPropertyDescriptor(cur, prop);
355
- if (descriptor) return !!descriptor.writable;
356
- cur = Object.getPrototypeOf(cur);
357
- }
358
- return true;
359
- };
360
-
361
- /**
362
- * Handles sanitization of Express request objects.
363
- * @param {Object} request - Express request object
364
- * @param {Object} options - Sanitization options
365
- */
366
- const handleRequest = (request, options) => {
367
- const { sanitizeObjects, customSanitizer, debug } = options;
368
- log(debug, 'info', 'REQUEST', `Sanitizing request`, { url: request.originalUrl || request.url });
369
- sanitizeObjects.forEach((sanitizeObject) => {
370
- const requestObject = request[sanitizeObject];
371
- if (requestObject && !isObjectEmpty(requestObject)) {
372
- log(debug, 'debug', 'REQUEST', `Sanitizing '${sanitizeObject}'`, requestObject);
373
- const originalRequest = Object.assign(Array.isArray(requestObject) ? [] : {}, requestObject);
374
- const sanitized = customSanitizer
375
- ? customSanitizer(originalRequest, options)
376
- : sanitizeValue(originalRequest, options);
377
- if (debug?.enabled && JSON.stringify(originalRequest) !== JSON.stringify(sanitized)) {
378
- log(debug, 'debug', 'REQUEST', `'${sanitizeObject}' sanitized`, {
379
- before: originalRequest,
380
- after: sanitized,
381
- });
382
- }
383
- if (isWritable(request, sanitizeObject)) {
384
- request[sanitizeObject] = sanitized;
385
- } else if (isPlainObject(request[sanitizeObject]) && sanitizeObject === 'query') {
386
- Object.defineProperty(request, 'query', {
387
- value: Object.setPrototypeOf(sanitized, null),
388
- writable: true,
389
- enumerable: true,
390
- configurable: true,
391
- });
392
- }
393
- }
394
- });
395
- };
396
-
397
- /**
398
- * Cleans and normalizes a URL path for comparison.
399
- * @param {string} url - URL to clean
400
- * @returns {string|null} Cleaned URL path or null if invalid
401
- */
402
- const cleanUrl = (url) => {
403
- if (typeof url !== 'string' || !url) return null;
404
- const [path] = url.split(/[?#]/);
405
- const trimmed = path.replace(/^\/+|\/+$/g, '');
406
- return trimmed ? '/' + trimmed : '/';
407
- };
408
-
409
- /**
410
- * Main middleware factory function for Express MongoDB sanitization.
411
- * @param {Object} [options={}] - Configuration options
412
- * @returns {Function} Express middleware function
413
- * @throws {ExpressMongoSanitizeError} If options are invalid
414
- */
415
- const expressMongoSanitize = (options = {}) => {
416
- if (!isPlainObject(options)) throw new ExpressMongoSanitizeError('Options must be an object', 'type_error');
417
-
418
- const userOpts = { ...DEFAULT_OPTIONS, ...options };
419
- validateOptions(userOpts);
420
-
421
- const opts = {
422
- ...userOpts,
423
- skipRoutes: new Set(options.skipRoutes || DEFAULT_OPTIONS.skipRoutes),
424
- allowedKeys: new Set(options.allowedKeys || DEFAULT_OPTIONS.allowedKeys),
425
- deniedKeys: new Set(options.deniedKeys || DEFAULT_OPTIONS.deniedKeys),
426
- debug: { ...DEFAULT_OPTIONS.debug, ...(options.debug || {}) },
427
- };
428
-
429
- return (req, res, next) => {
430
- log(opts.debug, 'trace', 'MIDDLEWARE', `Incoming request`, { url: req.originalUrl || req.url, method: req.method });
431
- const cleanedRequestPath = cleanUrl(req.path || req.url);
432
- const shouldSkip = Array.from(opts.skipRoutes).some((skipPath) => cleanUrl(skipPath) === cleanedRequestPath);
433
- if (shouldSkip) {
434
- if (opts.debug?.logSkippedRoutes)
435
- log(opts.debug, 'info', 'SKIP', `Skipped route: ${req.method} ${cleanedRequestPath}`);
436
- return next();
437
- }
438
- if (opts.mode === 'auto') {
439
- log(opts.debug, 'trace', 'MIDDLEWARE', `Auto mode: running sanitizer`);
440
- handleRequest(req, opts);
441
- }
442
- if (opts.mode === 'manual') {
443
- log(opts.debug, 'trace', 'MIDDLEWARE', `Manual mode: exposing req.sanitize`);
444
- req.sanitize = (customOpts) => {
445
- const finalOpts = { ...opts, ...customOpts };
446
- handleRequest(req, finalOpts);
447
- };
448
- }
449
- next();
450
- };
451
- };
452
-
453
- /**
454
- * Creates a parameter sanitization handler for Express route parameters.
455
- * @param {Object} [options={}] - Configuration options
456
- * @returns {Function} Express parameter handler function
457
- */
458
- const paramSanitizeHandler = (options = {}) => {
459
- const opts = {
460
- ...DEFAULT_OPTIONS,
461
- ...options,
462
- debug: { ...DEFAULT_OPTIONS.debug, ...(options.debug || {}) },
463
- };
464
- return function (req, res, next, value, paramName) {
465
- const key = paramName || this?.name;
466
- if (key && req.params && isString(value)) {
467
- const before = req.params[key];
468
- req.params[key] = sanitizeString(value, opts, true);
469
- log(opts.debug, 'debug', 'PARAM', `Sanitized param '${key}'`, { before, after: req.params[key] });
470
- }
471
- next();
472
- };
473
- };
474
-
475
- module.exports = expressMongoSanitize;
476
- module.exports.default = expressMongoSanitize;
477
- module.exports.expressMongoSanitize = expressMongoSanitize;
478
- module.exports.paramSanitizeHandler = paramSanitizeHandler;
479
- exports.default = expressMongoSanitize;