@exortek/express-mongo-sanitize 1.1.1 → 2.0.1
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 +51 -222
- package/package.json +40 -28
- package/src/index.js +64 -0
- package/types/index.d.ts +47 -72
- package/LICENSE +0 -21
- package/index.js +0 -479
package/README.md
CHANGED
|
@@ -1,269 +1,98 @@
|
|
|
1
1
|
# @exortek/express-mongo-sanitize
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
+
Express middleware for NoSQL injection prevention. Sanitizes request `body`, `query`, and `params` to protect MongoDB queries from operator injection attacks.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|--------------------|:---------------:|
|
|
10
|
-
| `^1.x` | `^4.x` |
|
|
11
|
-
| `^1.x` | `^5.x` |
|
|
10
|
+
## 📦 Installation
|
|
12
11
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
58
|
+
## 🛠 Features
|
|
76
59
|
|
|
77
|
-
|
|
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
|
-
|
|
122
|
-
app.use(expressMongoSanitize({ mode: 'manual' }));
|
|
67
|
+
app.param('username', paramSanitizeHandler());
|
|
123
68
|
|
|
124
|
-
app.
|
|
125
|
-
|
|
126
|
-
res.json(req.
|
|
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
|
-
|
|
77
|
+
If you need fine-grained control over when sanitization occurs:
|
|
147
78
|
|
|
148
79
|
```js
|
|
149
|
-
app.use(
|
|
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
|
-
###
|
|
88
|
+
### Content-Type Guard
|
|
239
89
|
|
|
240
|
-
By default, only `
|
|
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.
|
|
93
|
+
app.use(mongoSanitize({ contentTypes: ['application/json', 'application/graphql'] }));
|
|
245
94
|
```
|
|
246
95
|
|
|
247
|
-
|
|
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
|
-
|
|
98
|
+
[MIT](../../LICENSE) — Created by **ExorTek**
|
package/package.json
CHANGED
|
@@ -1,56 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exortek/express-mongo-sanitize",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "2.0.1",
|
|
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
|
+
"import": "./src/index.js",
|
|
11
|
+
"require": "./src/index.js",
|
|
12
|
+
"types": "./types/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
8
15
|
"scripts": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"test:node": "node --test"
|
|
16
|
+
"test": "node --test test/*.test.js",
|
|
17
|
+
"prepublishOnly": "npm test"
|
|
12
18
|
},
|
|
13
19
|
"repository": {
|
|
14
20
|
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/ExorTek/
|
|
21
|
+
"url": "git+https://github.com/ExorTek/nosql-sanitize.git",
|
|
22
|
+
"directory": "packages/express"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/ExorTek/nosql-sanitize/packages/express#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ExorTek/nosql-sanitize/issues"
|
|
16
27
|
},
|
|
17
28
|
"keywords": [
|
|
18
29
|
"express",
|
|
19
30
|
"mongodb",
|
|
20
31
|
"sanitize",
|
|
21
|
-
"
|
|
22
|
-
"express-middleware",
|
|
23
|
-
"data-validation",
|
|
24
|
-
"input-sanitization",
|
|
32
|
+
"nosql",
|
|
25
33
|
"security",
|
|
26
|
-
"web-application",
|
|
27
|
-
"nodejs",
|
|
28
|
-
"typescript",
|
|
29
34
|
"middleware",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"no-sql",
|
|
33
|
-
"mongodb-sanitizer"
|
|
35
|
+
"injection",
|
|
36
|
+
"backend"
|
|
34
37
|
],
|
|
35
38
|
"author": "ExorTek - https://github.com/ExorTek",
|
|
36
39
|
"license": "MIT",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
39
42
|
},
|
|
40
|
-
"homepage": "https://github.com/ExorTek/express-mongo-sanitize#readme",
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
|
43
45
|
},
|
|
44
|
-
"
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@exortek/nosql-sanitize-core": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"express": ">=4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"express": {
|
|
54
|
+
"optional": false
|
|
55
|
+
}
|
|
56
|
+
},
|
|
45
57
|
"devDependencies": {
|
|
46
|
-
"@types/express": "^5.0.
|
|
58
|
+
"@types/express": "^5.0.6",
|
|
47
59
|
"express": "npm:express@5.1.0",
|
|
48
|
-
"express4": "npm:express@4.21.2"
|
|
49
|
-
"prettier": "^3.5.3",
|
|
50
|
-
"tsd": "^0.32.0"
|
|
60
|
+
"express4": "npm:express@4.21.2"
|
|
51
61
|
},
|
|
52
62
|
"files": [
|
|
53
|
-
"
|
|
54
|
-
"types/
|
|
63
|
+
"src/",
|
|
64
|
+
"types/",
|
|
65
|
+
"README.md",
|
|
66
|
+
"LICENSE"
|
|
55
67
|
]
|
|
56
68
|
}
|
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
|
-
|
|
1
|
+
/// <reference types="node" />
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|