@exortek/nosql-sanitize-core 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 +79 -0
- package/package.json +50 -0
- package/src/constants.js +56 -0
- package/src/errors.js +18 -0
- package/src/helpers.js +137 -0
- package/src/index.js +266 -0
- package/src/sanitizers.js +145 -0
- package/types/index.d.ts +255 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @exortek/nosql-sanitize-core
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> This is an internal core package for the `nosql-sanitize` monorepo. It is not intended for standalone public use.
|
|
5
|
+
|
|
6
|
+
Core sanitization engine for NoSQL injection prevention. This package provides the high-performance logic used by framework-specific adapters.
|
|
7
|
+
|
|
8
|
+
## 📦 Usage
|
|
9
|
+
|
|
10
|
+
Most users should install the framework-specific package ([`express-mongo-sanitize`](../express) or [`fastify-mongo-sanitize`](../fastify)) instead. Use this package directly only if you're building a custom integration or need standalone sanitization.
|
|
11
|
+
|
|
12
|
+
### Installation (Internal Only)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @exortek/nosql-sanitize-core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 🚀 API Reference
|
|
19
|
+
|
|
20
|
+
### `sanitizeValue(value, options, isValue?, depth?)`
|
|
21
|
+
|
|
22
|
+
The main entry point for sanitization. It intelligently dispatches to specialized sanitizers based on the data type.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const { sanitizeValue, resolveOptions } = require('@exortek/nosql-sanitize-core');
|
|
26
|
+
|
|
27
|
+
const opts = resolveOptions();
|
|
28
|
+
sanitizeValue('$admin', opts, true); // → 'admin'
|
|
29
|
+
sanitizeValue({ $gt: 1 }, opts); // → { gt: 1 }
|
|
30
|
+
sanitizeValue(['$a', '$b'], opts); // → ['a', 'b']
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `resolveOptions(options?)`
|
|
34
|
+
|
|
35
|
+
Merges user-provided options with defaults, pre-compiles regex patterns, and validates the configuration.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const { resolveOptions } = require('@exortek/nosql-sanitize-core');
|
|
39
|
+
|
|
40
|
+
const opts = resolveOptions({
|
|
41
|
+
replaceWith: '_',
|
|
42
|
+
maxDepth: 5,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `handleRequest(request, options)`
|
|
47
|
+
|
|
48
|
+
A utility function to sanitize a request object's fields (`body`, `query`, `params`) in-place. Used by Express and Fastify adapters.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## ⚙️ Configuration Options
|
|
53
|
+
|
|
54
|
+
| Option | Type | Default | Description |
|
|
55
|
+
|:-------|:-----|:--------|:------------|
|
|
56
|
+
| `replaceWith` | `string` | `''` | String to replace matched patterns with. |
|
|
57
|
+
| `removeMatches` | `boolean` | `false` | If `true`, removes the entire key-value pair if a match is found. |
|
|
58
|
+
| `sanitizeObjects` | `string[]` | `['body', 'query']` | Fields on the request object to sanitize. |
|
|
59
|
+
| `contentTypes` | `string[] \| null` | `[...]` | Only sanitize `body` for these content types. `null` = all. |
|
|
60
|
+
| `mode` | `'auto' \| 'manual'` | `'auto'` | Automatically sanitize or expose `req.sanitize()`. |
|
|
61
|
+
| `skipRoutes` | `(string \| RegExp)[]` | `[]` | Routes to ignore during auto-sanitization. |
|
|
62
|
+
| `maxDepth` | `number \| null` | `null` | Maximum recursion depth for nested structures. |
|
|
63
|
+
| `recursive` | `boolean` | `true` | Whether to recursively sanitize nested objects/arrays. |
|
|
64
|
+
| `onSanitize` | `function` | `null` | Hook called when a value is sanitized. |
|
|
65
|
+
| `allowedKeys` | `string[]` | `[]` | Whitelist of keys to allow without sanitization. |
|
|
66
|
+
| `deniedKeys` | `string[]` | `[]` | Blacklist of keys to completely remove. |
|
|
67
|
+
|
|
68
|
+
## 🔍 Default Patterns
|
|
69
|
+
|
|
70
|
+
The core engine targets common MongoDB injection vectors:
|
|
71
|
+
|
|
72
|
+
1. **Operator Prefix**: `$` (Matches characters used for `$gt`, `$ne`, `$where`, etc.)
|
|
73
|
+
2. **Control Characters**: Null bytes and C0/C1 control characters (`\u0000-\u001F`).
|
|
74
|
+
|
|
75
|
+
You can override these by passing a `patterns` array in the options.
|
|
76
|
+
|
|
77
|
+
## 📜 License
|
|
78
|
+
|
|
79
|
+
[MIT](../../LICENSE) — Created by **ExorTek**
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exortek/nosql-sanitize-core",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Core sanitization engine for NoSQL injection prevention",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": "./src/index.js",
|
|
11
|
+
"types": "./types/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test test/*.test.js",
|
|
16
|
+
"prepublishOnly": "npm test"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/ExorTek/nosql-sanitize.git",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/ExorTek/nosql-sanitize/tree/main/packages/core#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/ExorTek/nosql-sanitize/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"nosql",
|
|
29
|
+
"sanitize",
|
|
30
|
+
"mongodb",
|
|
31
|
+
"injection",
|
|
32
|
+
"security",
|
|
33
|
+
"core",
|
|
34
|
+
"backend"
|
|
35
|
+
],
|
|
36
|
+
"author": "ExorTek - https://github.com/ExorTek",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"src/",
|
|
46
|
+
"types/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
]
|
|
50
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PATTERNS = Object.freeze([/\$/g, /[\u0000-\u001F\u007F-\u009F]/g]);
|
|
4
|
+
|
|
5
|
+
const LOG_LEVELS = Object.freeze({
|
|
6
|
+
silent: 0,
|
|
7
|
+
error: 1,
|
|
8
|
+
warn: 2,
|
|
9
|
+
info: 3,
|
|
10
|
+
debug: 4,
|
|
11
|
+
trace: 5,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const LOG_COLORS = Object.freeze({
|
|
15
|
+
error: '\x1b[31m',
|
|
16
|
+
warn: '\x1b[33m',
|
|
17
|
+
info: '\x1b[36m',
|
|
18
|
+
debug: '\x1b[90m',
|
|
19
|
+
trace: '\x1b[35m',
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const DEFAULT_OPTIONS = Object.freeze({
|
|
24
|
+
replaceWith: '',
|
|
25
|
+
removeMatches: false,
|
|
26
|
+
sanitizeObjects: ['body', 'query'],
|
|
27
|
+
contentTypes: ['application/json', 'application/x-www-form-urlencoded'],
|
|
28
|
+
mode: 'auto',
|
|
29
|
+
skipRoutes: [],
|
|
30
|
+
customSanitizer: null,
|
|
31
|
+
onSanitize: null,
|
|
32
|
+
recursive: true,
|
|
33
|
+
removeEmpty: false,
|
|
34
|
+
maxDepth: null,
|
|
35
|
+
patterns: PATTERNS,
|
|
36
|
+
allowedKeys: [],
|
|
37
|
+
deniedKeys: [],
|
|
38
|
+
stringOptions: {
|
|
39
|
+
trim: false,
|
|
40
|
+
lowercase: false,
|
|
41
|
+
maxLength: null,
|
|
42
|
+
},
|
|
43
|
+
arrayOptions: {
|
|
44
|
+
filterNull: false,
|
|
45
|
+
distinct: false,
|
|
46
|
+
},
|
|
47
|
+
debug: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
level: 'info',
|
|
50
|
+
logPatternMatches: false,
|
|
51
|
+
logSanitizedValues: false,
|
|
52
|
+
logSkippedRoutes: false,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
module.exports = { PATTERNS, LOG_LEVELS, LOG_COLORS, DEFAULT_OPTIONS };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class NoSQLSanitizeError extends Error {
|
|
4
|
+
constructor(message, type = 'generic') {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'NoSQLSanitizeError';
|
|
7
|
+
this.type = type;
|
|
8
|
+
if (Error.captureStackTrace) {
|
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
code() {
|
|
14
|
+
return this.type;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { NoSQLSanitizeError };
|
package/src/helpers.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LOG_LEVELS, LOG_COLORS } = require('./constants');
|
|
4
|
+
const { NoSQLSanitizeError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
const isString = (value) => typeof value === 'string';
|
|
7
|
+
const isArray = (value) => Array.isArray(value);
|
|
8
|
+
const isBoolean = (value) => typeof value === 'boolean';
|
|
9
|
+
const isNumber = (value) => typeof value === 'number';
|
|
10
|
+
const isPrimitive = (value) => value === null || typeof value === 'boolean' || typeof value === 'number';
|
|
11
|
+
const isDate = (value) => value instanceof Date;
|
|
12
|
+
const isFunction = (value) => typeof value === 'function';
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (obj) => {
|
|
15
|
+
if (obj === null || typeof obj !== 'object' || isArray(obj) || obj instanceof Date || obj instanceof RegExp) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const proto = Object.getPrototypeOf(obj);
|
|
19
|
+
if (proto === Object.prototype || proto === null) return true;
|
|
20
|
+
// Fastify query objects: proto is a null-prototype object (2 levels deep)
|
|
21
|
+
return Object.getPrototypeOf(proto) === null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isObjectEmpty = (obj) => {
|
|
25
|
+
if (!isPlainObject(obj)) return false;
|
|
26
|
+
for (const key in obj) {
|
|
27
|
+
if (Object.hasOwn(obj, key)) return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const EMAIL_RE = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i;
|
|
33
|
+
const isEmail = (val) => isString(val) && val.length > 5 && val.indexOf('@') > 0 && EMAIL_RE.test(val);
|
|
34
|
+
|
|
35
|
+
const cleanUrl = (url) => {
|
|
36
|
+
if (!isString(url) || url.length === 0) return null;
|
|
37
|
+
|
|
38
|
+
// Strip query string and hash — indexOf faster than regex
|
|
39
|
+
let end = url.length;
|
|
40
|
+
const qIdx = url.indexOf('?');
|
|
41
|
+
const hIdx = url.indexOf('#');
|
|
42
|
+
if (qIdx !== -1 && qIdx < end) end = qIdx;
|
|
43
|
+
if (hIdx !== -1 && hIdx < end) end = hIdx;
|
|
44
|
+
|
|
45
|
+
// Trim leading/trailing slashes
|
|
46
|
+
let start = 0;
|
|
47
|
+
while (start < end && url.charCodeAt(start) === 47) start++;
|
|
48
|
+
while (end > start && url.charCodeAt(end - 1) === 47) end--;
|
|
49
|
+
|
|
50
|
+
if (start >= end) return null;
|
|
51
|
+
return '/' + url.slice(start, end);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the mime type from a content-type header value.
|
|
56
|
+
* "application/json; charset=utf-8" → "application/json"
|
|
57
|
+
*/
|
|
58
|
+
const extractMimeType = (contentType) => {
|
|
59
|
+
if (!isString(contentType)) return null;
|
|
60
|
+
const idx = contentType.indexOf(';');
|
|
61
|
+
const mime = (idx === -1 ? contentType : contentType.slice(0, idx)).trim().toLowerCase();
|
|
62
|
+
return mime || null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const log = (debugOpts, level, context, message, data = null) => {
|
|
66
|
+
if (!debugOpts?.enabled || LOG_LEVELS[debugOpts.level || 'silent'] < LOG_LEVELS[level]) return;
|
|
67
|
+
|
|
68
|
+
const color = LOG_COLORS[level] || '';
|
|
69
|
+
const reset = LOG_COLORS.reset;
|
|
70
|
+
const timestamp = new Date().toISOString();
|
|
71
|
+
const logMessage = `${color}[nosql-sanitize:${level.toUpperCase()}]${reset} ${timestamp} [${context}] ${message}`;
|
|
72
|
+
|
|
73
|
+
if (data !== null && typeof data === 'object') {
|
|
74
|
+
console.log(logMessage);
|
|
75
|
+
console.log(`${color}Data:${reset}`, JSON.stringify(data, null, 2));
|
|
76
|
+
} else if (data !== null) {
|
|
77
|
+
console.log(logMessage, data);
|
|
78
|
+
} else {
|
|
79
|
+
console.log(logMessage);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const startTiming = (debugOpts, operation) => {
|
|
84
|
+
if (!debugOpts?.enabled) return () => {};
|
|
85
|
+
const start = process.hrtime();
|
|
86
|
+
log(debugOpts, 'trace', 'TIMING', `Started: ${operation}`);
|
|
87
|
+
return () => {
|
|
88
|
+
const [s, ns] = process.hrtime(start);
|
|
89
|
+
log(debugOpts, 'trace', 'TIMING', `Completed: ${operation} in ${(s * 1000 + ns / 1e6).toFixed(2)}ms`);
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const validators = Object.freeze({
|
|
94
|
+
replaceWith: isString,
|
|
95
|
+
removeMatches: isBoolean,
|
|
96
|
+
sanitizeObjects: isArray,
|
|
97
|
+
mode: (v) => ['auto', 'manual'].includes(v),
|
|
98
|
+
skipRoutes: isArray,
|
|
99
|
+
contentTypes: (v) => v === null || isArray(v),
|
|
100
|
+
customSanitizer: (v) => v === null || isFunction(v),
|
|
101
|
+
onSanitize: (v) => v === null || isFunction(v),
|
|
102
|
+
recursive: isBoolean,
|
|
103
|
+
removeEmpty: isBoolean,
|
|
104
|
+
maxDepth: (v) => v === null || (isNumber(v) && v > 0),
|
|
105
|
+
patterns: isArray,
|
|
106
|
+
allowedKeys: (v) => v === null || isArray(v),
|
|
107
|
+
deniedKeys: (v) => v === null || isArray(v),
|
|
108
|
+
stringOptions: isPlainObject,
|
|
109
|
+
arrayOptions: isPlainObject,
|
|
110
|
+
debug: isPlainObject,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const validateOptions = (options) => {
|
|
114
|
+
for (const [key, validate] of Object.entries(validators)) {
|
|
115
|
+
if (options[key] !== undefined && !validate(options[key])) {
|
|
116
|
+
throw new NoSQLSanitizeError(`Invalid configuration: "${key}"`, 'type_error');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
isString,
|
|
123
|
+
isArray,
|
|
124
|
+
isPlainObject,
|
|
125
|
+
isBoolean,
|
|
126
|
+
isNumber,
|
|
127
|
+
isPrimitive,
|
|
128
|
+
isDate,
|
|
129
|
+
isFunction,
|
|
130
|
+
isObjectEmpty,
|
|
131
|
+
isEmail,
|
|
132
|
+
cleanUrl,
|
|
133
|
+
extractMimeType,
|
|
134
|
+
log,
|
|
135
|
+
startTiming,
|
|
136
|
+
validateOptions,
|
|
137
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { DEFAULT_OPTIONS, PATTERNS, LOG_LEVELS, LOG_COLORS } = require('./constants');
|
|
4
|
+
const { NoSQLSanitizeError } = require('./errors');
|
|
5
|
+
const { sanitizeString, sanitizeArray, sanitizeObject, sanitizeValue } = require('./sanitizers');
|
|
6
|
+
const helpers = require('./helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves and validates configuration options by merging user-provided options
|
|
10
|
+
* with default options and applying additional validations and transformations.
|
|
11
|
+
* This function ensures that nested configuration objects are properly validated,
|
|
12
|
+
* and prepares the resolved options for efficient runtime use.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} [userOptions={}] - The user-provided configuration options.
|
|
15
|
+
* @throws {NoSQLSanitizeError} Throws an error if the provided options or nested
|
|
16
|
+
* objects do not have the expected types.
|
|
17
|
+
* @returns {Object} The fully resolved and validated options object, ready for use.
|
|
18
|
+
*/
|
|
19
|
+
const resolveOptions = (userOptions = {}) => {
|
|
20
|
+
// Ensure the base options argument is a valid plain object to prevent prototype pollution or type errors
|
|
21
|
+
if (!helpers.isPlainObject(userOptions)) {
|
|
22
|
+
throw new NoSQLSanitizeError('Options must be an object', 'type_error');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate nested object types before merge to avoid corrupting the configuration structure
|
|
26
|
+
if (userOptions.debug !== undefined && !helpers.isPlainObject(userOptions.debug)) {
|
|
27
|
+
throw new NoSQLSanitizeError('Invalid configuration: "debug"', 'type_error');
|
|
28
|
+
}
|
|
29
|
+
if (userOptions.stringOptions !== undefined && !helpers.isPlainObject(userOptions.stringOptions)) {
|
|
30
|
+
throw new NoSQLSanitizeError('Invalid configuration: "stringOptions"', 'type_error');
|
|
31
|
+
}
|
|
32
|
+
if (userOptions.arrayOptions !== undefined && !helpers.isPlainObject(userOptions.arrayOptions)) {
|
|
33
|
+
throw new NoSQLSanitizeError('Invalid configuration: "arrayOptions"', 'type_error');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deep merge default options with user-provided options safely
|
|
37
|
+
const opts = {
|
|
38
|
+
...DEFAULT_OPTIONS,
|
|
39
|
+
...userOptions,
|
|
40
|
+
stringOptions: { ...DEFAULT_OPTIONS.stringOptions, ...(userOptions.stringOptions || {}) },
|
|
41
|
+
arrayOptions: { ...DEFAULT_OPTIONS.arrayOptions, ...(userOptions.arrayOptions || {}) },
|
|
42
|
+
debug: { ...DEFAULT_OPTIONS.debug, ...(userOptions.debug || {}) },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
helpers.validateOptions(opts);
|
|
46
|
+
|
|
47
|
+
// Pre-compile combined pattern (just once) for performance optimization during runtime
|
|
48
|
+
const patterns = opts.patterns || PATTERNS;
|
|
49
|
+
opts._combinedPattern = new RegExp(patterns.map((p) => p.source).join('|'), 'g');
|
|
50
|
+
|
|
51
|
+
// Parse skipRoutes: Separate exact string matches (converted to a Set for O(1) lookup)
|
|
52
|
+
// and RegExp patterns (kept in an array) for faster evaluation later
|
|
53
|
+
const rawSkipRoutes = userOptions.skipRoutes || [];
|
|
54
|
+
const exactSkipRoutes = new Set();
|
|
55
|
+
const regexSkipRoutes = [];
|
|
56
|
+
|
|
57
|
+
for (const route of rawSkipRoutes) {
|
|
58
|
+
if (route instanceof RegExp) {
|
|
59
|
+
regexSkipRoutes.push(route);
|
|
60
|
+
} else if (helpers.isString(route)) {
|
|
61
|
+
const cleaned = helpers.cleanUrl(route);
|
|
62
|
+
if (cleaned) exactSkipRoutes.add(cleaned);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
opts.skipRoutes = { exact: exactSkipRoutes, regex: regexSkipRoutes };
|
|
67
|
+
|
|
68
|
+
// Normalize contentTypes to a Set of lowercase strings for case-insensitive O(1) lookups.
|
|
69
|
+
// A null value indicates that content-type checking should be bypassed entirely.
|
|
70
|
+
const rawContentTypes =
|
|
71
|
+
userOptions.contentTypes !== undefined ? userOptions.contentTypes : DEFAULT_OPTIONS.contentTypes;
|
|
72
|
+
opts.contentTypes = rawContentTypes === null ? null : new Set(rawContentTypes.map((ct) => ct.toLowerCase()));
|
|
73
|
+
|
|
74
|
+
// Convert allowed and denied keys to Sets for faster inclusion checks
|
|
75
|
+
opts.allowedKeys = new Set(userOptions.allowedKeys || []);
|
|
76
|
+
opts.deniedKeys = new Set(userOptions.deniedKeys || []);
|
|
77
|
+
|
|
78
|
+
// Set max depth constraint for nested object parsing to prevent stack overflow/ReDoS attacks
|
|
79
|
+
opts.maxDepth = userOptions.maxDepth !== undefined ? userOptions.maxDepth : null;
|
|
80
|
+
|
|
81
|
+
// Assign the optional custom callback for post-sanitization hooks
|
|
82
|
+
opts.onSanitize = userOptions.onSanitize || null;
|
|
83
|
+
|
|
84
|
+
return opts;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determines whether a specified property on an object is writable.
|
|
89
|
+
*
|
|
90
|
+
* This function checks if the property on the given object or its prototype chain
|
|
91
|
+
* has a descriptor indicating it is writable (either the `writable` attribute is true
|
|
92
|
+
* or a `set` accessor exists). If the property cannot be found in the chain, it defaults to true.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} obj - The object to inspect.
|
|
95
|
+
* @param {string} prop - The name of the property to check for writability.
|
|
96
|
+
* @returns {boolean} Returns `true` if the property is writable or defaults to
|
|
97
|
+
* writable when no descriptor is found; otherwise, `false`.
|
|
98
|
+
*/
|
|
99
|
+
const isWritable = (obj, prop) => {
|
|
100
|
+
let cur = obj;
|
|
101
|
+
// Traverse up the prototype chain to find the property descriptor
|
|
102
|
+
while (cur) {
|
|
103
|
+
const desc = Object.getOwnPropertyDescriptor(cur, prop);
|
|
104
|
+
// If the descriptor is found, check if it allows assignment (writable or has a setter)
|
|
105
|
+
if (desc) return !!desc.writable || !!desc.set;
|
|
106
|
+
cur = Object.getPrototypeOf(cur);
|
|
107
|
+
}
|
|
108
|
+
// Default to true if no descriptor restricts it in the entire prototype chain
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Determines whether a request's content type should be sanitized.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} request - The request object, which contains headers and may include a `get` method to retrieve headers.
|
|
116
|
+
* @param {Array<string>} contentTypes - A set of allowed MIME types or null to sanitize all content types.
|
|
117
|
+
* @returns {boolean} - Returns true if the content type should be sanitized, otherwise false.
|
|
118
|
+
*/
|
|
119
|
+
const shouldSanitizeContentType = (request, contentTypes) => {
|
|
120
|
+
// If contentTypes is explicitly null, bypass the check and sanitize everything
|
|
121
|
+
if (contentTypes === null) return true;
|
|
122
|
+
|
|
123
|
+
// Safely extract the content-type header, supporting both standard Node.js req.headers
|
|
124
|
+
// and Express.js req.get() method
|
|
125
|
+
const header =
|
|
126
|
+
(request.headers && request.headers['content-type']) ||
|
|
127
|
+
(typeof request.get === 'function' && request.get('content-type')) ||
|
|
128
|
+
null;
|
|
129
|
+
|
|
130
|
+
// If there is no content-type header (e.g., simple GET requests), default to sanitizing
|
|
131
|
+
if (!header) return true;
|
|
132
|
+
|
|
133
|
+
// Extract the base MIME type (ignoring charsets/boundaries) and check against the allowed Set
|
|
134
|
+
const mime = helpers.extractMimeType(header);
|
|
135
|
+
return mime ? contentTypes.has(mime) : true;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handles the sanitization of a request object based on the provided options.
|
|
140
|
+
*
|
|
141
|
+
* This function processes specified fields within the request (`sanitizeObjects`)
|
|
142
|
+
* and sanitizes their values using either a custom sanitizer or a default sanitizer.
|
|
143
|
+
* It conditionally skips sanitization for the `body` field if its content type
|
|
144
|
+
* is not included in the allowed list (`contentTypes`).
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} request - The HTTP request object to be sanitized.
|
|
147
|
+
* @param {Object} options - Configuration options for the sanitization process.
|
|
148
|
+
* @param {Array.<string>} options.sanitizeObjects - List of request fields to be sanitized (e.g., `body`, `query`, `params`).
|
|
149
|
+
* @param {Function} [options.customSanitizer] - Optional custom function to handle the sanitization of field values.
|
|
150
|
+
* @param {boolean} [options.debug] - Flag to enable or disable debug logging.
|
|
151
|
+
* @param {Array.<string>} [options.contentTypes] - Allowed content types that determine whether the `body` field is sanitized.
|
|
152
|
+
*/
|
|
153
|
+
const handleRequest = (request, options) => {
|
|
154
|
+
const { sanitizeObjects, customSanitizer, debug, contentTypes } = options;
|
|
155
|
+
const endTiming = helpers.startTiming(debug, 'Request Sanitization');
|
|
156
|
+
|
|
157
|
+
helpers.log(debug, 'info', 'REQUEST', 'Sanitizing request');
|
|
158
|
+
|
|
159
|
+
// Determine early on if the 'body' payload should be processed based on its MIME type
|
|
160
|
+
const shouldSanitizeBody = shouldSanitizeContentType(request, contentTypes);
|
|
161
|
+
|
|
162
|
+
for (const field of sanitizeObjects) {
|
|
163
|
+
// Skip 'body' specifically if the content-type validation failed
|
|
164
|
+
if (field === 'body' && !shouldSanitizeBody) {
|
|
165
|
+
helpers.log(debug, 'debug', 'REQUEST', 'Skipping body — content-type not in allowed list');
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const data = request[field];
|
|
170
|
+
|
|
171
|
+
// Skip processing if the payload field is undefined or null
|
|
172
|
+
if (data == null) continue;
|
|
173
|
+
|
|
174
|
+
// Avoid running expensive sanitization logic on completely empty objects
|
|
175
|
+
if (helpers.isPlainObject(data) && helpers.isObjectEmpty(data)) continue;
|
|
176
|
+
|
|
177
|
+
helpers.log(debug, 'debug', 'REQUEST', `Sanitizing '${field}'`);
|
|
178
|
+
|
|
179
|
+
// Create a shallow clone of the data to avoid mutating references unexpectedly
|
|
180
|
+
// before applying the sanitization logic
|
|
181
|
+
const original = Array.isArray(data) ? [...data] : helpers.isPlainObject(data) ? { ...data } : data;
|
|
182
|
+
|
|
183
|
+
// Route the data through a custom sanitizer if provided, otherwise use the internal one
|
|
184
|
+
const sanitized = customSanitizer ? customSanitizer(original, options) : sanitizeValue(original, options);
|
|
185
|
+
|
|
186
|
+
// Specific workaround for Express 5+: 'req.query' might be defined as non-writable via getter/setter.
|
|
187
|
+
// If it's writable, do a standard assignment. If not, forcefully redefine the property.
|
|
188
|
+
if (isWritable(request, field)) {
|
|
189
|
+
request[field] = sanitized;
|
|
190
|
+
} else {
|
|
191
|
+
Object.defineProperty(request, field, {
|
|
192
|
+
value: sanitized,
|
|
193
|
+
writable: true,
|
|
194
|
+
enumerable: true,
|
|
195
|
+
configurable: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
endTiming();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Determines whether a given route should be skipped based on exact matches or regex patterns.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} requestPath - The request path to be evaluated.
|
|
207
|
+
* @param {Object} skipRoutes - An object containing route skipping criteria.
|
|
208
|
+
* @param {Set<string>} skipRoutes.exact - A set of exact route paths to skip.
|
|
209
|
+
* @param {RegExp[]} skipRoutes.regex - An array of regular expressions to test against the route path.
|
|
210
|
+
* @param {Object} [debug] - Optional debugging configuration.
|
|
211
|
+
* @param {boolean} [debug.logSkippedRoutes] - Flag indicating whether skipped routes should be logged.
|
|
212
|
+
* @returns {boolean} `true` if the route should be skipped, otherwise `false`.
|
|
213
|
+
*/
|
|
214
|
+
const shouldSkipRoute = (requestPath, skipRoutes, debug) => {
|
|
215
|
+
const { exact, regex } = skipRoutes;
|
|
216
|
+
|
|
217
|
+
// Quick return if there are no skip rules configured
|
|
218
|
+
if (!exact.size && !regex.length) return false;
|
|
219
|
+
|
|
220
|
+
const cleaned = helpers.cleanUrl(requestPath);
|
|
221
|
+
|
|
222
|
+
// Check the exact matches first (O(1) Set lookup) for better performance
|
|
223
|
+
if (cleaned && exact.has(cleaned)) {
|
|
224
|
+
if (debug?.logSkippedRoutes) {
|
|
225
|
+
helpers.log(debug, 'info', 'SKIP', `Route skipped (exact): ${requestPath}`);
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Fallback to iterating through RegExp patterns if no exact match is found
|
|
231
|
+
if (regex.length && cleaned) {
|
|
232
|
+
for (const pattern of regex) {
|
|
233
|
+
// Reset lastIndex to 0 to prevent issues with global (/g) regular expressions maintaining state
|
|
234
|
+
pattern.lastIndex = 0;
|
|
235
|
+
if (pattern.test(cleaned)) {
|
|
236
|
+
if (debug?.logSkippedRoutes) {
|
|
237
|
+
helpers.log(debug, 'info', 'SKIP', `Route skipped (regex ${pattern}): ${requestPath}`);
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
resolveOptions,
|
|
249
|
+
handleRequest,
|
|
250
|
+
shouldSkipRoute,
|
|
251
|
+
shouldSanitizeContentType,
|
|
252
|
+
isWritable,
|
|
253
|
+
|
|
254
|
+
sanitizeString,
|
|
255
|
+
sanitizeArray,
|
|
256
|
+
sanitizeObject,
|
|
257
|
+
sanitizeValue,
|
|
258
|
+
|
|
259
|
+
...helpers,
|
|
260
|
+
|
|
261
|
+
DEFAULT_OPTIONS,
|
|
262
|
+
PATTERNS,
|
|
263
|
+
LOG_LEVELS,
|
|
264
|
+
LOG_COLORS,
|
|
265
|
+
NoSQLSanitizeError,
|
|
266
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isString, isArray, isPlainObject, isPrimitive, isDate, isEmail, log } = require('./helpers');
|
|
4
|
+
const { NoSQLSanitizeError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitizes a string value.
|
|
8
|
+
*/
|
|
9
|
+
const sanitizeString = (str, options, isValue = false) => {
|
|
10
|
+
if (!isString(str) || isEmail(str)) return str;
|
|
11
|
+
|
|
12
|
+
const { replaceWith, stringOptions, debug, _combinedPattern } = options;
|
|
13
|
+
const original = str;
|
|
14
|
+
|
|
15
|
+
_combinedPattern.lastIndex = 0;
|
|
16
|
+
let result = str.replace(_combinedPattern, replaceWith);
|
|
17
|
+
|
|
18
|
+
if (stringOptions.trim) result = result.trim();
|
|
19
|
+
if (stringOptions.lowercase) result = result.toLowerCase();
|
|
20
|
+
if (stringOptions.maxLength && isValue) result = result.slice(0, stringOptions.maxLength);
|
|
21
|
+
|
|
22
|
+
if (debug?.enabled && original !== result) {
|
|
23
|
+
log(debug, 'debug', 'STRING', 'Sanitized', { original, result });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sanitizes an array.
|
|
31
|
+
*/
|
|
32
|
+
const sanitizeArray = (arr, options, depth) => {
|
|
33
|
+
if (!isArray(arr)) {
|
|
34
|
+
throw new NoSQLSanitizeError('Input must be an array', 'type_error');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { recursive, arrayOptions } = options;
|
|
38
|
+
const len = arr.length;
|
|
39
|
+
const result = new Array(len);
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
const item = arr[i];
|
|
43
|
+
if (!recursive && (isPlainObject(item) || isArray(item))) {
|
|
44
|
+
result[i] = item;
|
|
45
|
+
} else {
|
|
46
|
+
result[i] = sanitizeValue(item, options, true, depth);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!arrayOptions.filterNull && !arrayOptions.distinct) return result;
|
|
51
|
+
|
|
52
|
+
let out = result;
|
|
53
|
+
if (arrayOptions.filterNull) out = out.filter(Boolean);
|
|
54
|
+
if (arrayOptions.distinct) out = [...new Set(out)];
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sanitizes an object. Uses for..of Object.keys() — no tuple allocation.
|
|
60
|
+
*/
|
|
61
|
+
const sanitizeObject = (obj, options, depth) => {
|
|
62
|
+
if (!isPlainObject(obj)) {
|
|
63
|
+
throw new NoSQLSanitizeError('Input must be an object', 'type_error');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { removeEmpty, allowedKeys, deniedKeys, removeMatches, _combinedPattern, debug, recursive, onSanitize } =
|
|
67
|
+
options;
|
|
68
|
+
|
|
69
|
+
const acc = {};
|
|
70
|
+
const keys = Object.keys(obj);
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < keys.length; i++) {
|
|
73
|
+
const key = keys[i];
|
|
74
|
+
const val = obj[key];
|
|
75
|
+
|
|
76
|
+
// Denied key — email value korunur (BUG-03 fix)
|
|
77
|
+
if (deniedKeys.size && deniedKeys.has(key)) {
|
|
78
|
+
if (isEmail(val)) {
|
|
79
|
+
acc[sanitizeString(key, options)] = val;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
log(debug, 'debug', 'OBJECT', `Key '${key}' denied`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Allowed key filtresi
|
|
87
|
+
if (allowedKeys.size && !allowedKeys.has(key)) {
|
|
88
|
+
log(debug, 'debug', 'OBJECT', `Key '${key}' not in allowedKeys`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sanitizedKey = sanitizeString(key, options);
|
|
93
|
+
|
|
94
|
+
// removeMatches — tek _combinedPattern.test()
|
|
95
|
+
if (removeMatches) {
|
|
96
|
+
_combinedPattern.lastIndex = 0;
|
|
97
|
+
if (_combinedPattern.test(key)) continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (removeEmpty && !sanitizedKey) continue;
|
|
101
|
+
|
|
102
|
+
// removeMatches — value pattern match
|
|
103
|
+
if (removeMatches && isString(val)) {
|
|
104
|
+
_combinedPattern.lastIndex = 0;
|
|
105
|
+
if (_combinedPattern.test(val)) continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sanitizedValue =
|
|
109
|
+
!recursive && (isPlainObject(val) || isArray(val)) ? val : sanitizeValue(val, options, true, depth);
|
|
110
|
+
|
|
111
|
+
if (removeEmpty && !sanitizedValue) continue;
|
|
112
|
+
|
|
113
|
+
// onSanitize callback
|
|
114
|
+
if (onSanitize && isString(val) && val !== sanitizedValue) {
|
|
115
|
+
onSanitize({ key, originalValue: val, sanitizedValue, path: key });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
acc[sanitizedKey] = sanitizedValue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return acc;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Main dispatch — routes to appropriate sanitizer by type.
|
|
126
|
+
* @param {*} value
|
|
127
|
+
* @param {Object} options
|
|
128
|
+
* @param {boolean} isValue - true if this is a value (not a key)
|
|
129
|
+
* @param {number} depth - current recursion depth
|
|
130
|
+
*/
|
|
131
|
+
const sanitizeValue = (value, options, isValue = false, depth = 0) => {
|
|
132
|
+
if (value == null || isPrimitive(value) || isDate(value)) return value;
|
|
133
|
+
|
|
134
|
+
// Strings are always sanitized regardless of depth
|
|
135
|
+
if (isString(value)) return sanitizeString(value, options, isValue);
|
|
136
|
+
|
|
137
|
+
// maxDepth guard — stop recursing into nested objects/arrays
|
|
138
|
+
if (options.maxDepth !== null && depth >= options.maxDepth) return value;
|
|
139
|
+
|
|
140
|
+
if (isArray(value)) return sanitizeArray(value, options, depth + 1);
|
|
141
|
+
if (isPlainObject(value)) return sanitizeObject(value, options, depth + 1);
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
module.exports = { sanitizeString, sanitizeArray, sanitizeObject, sanitizeValue };
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
type NoSQLSanitizeCore = typeof noSQLSanitizeCore;
|
|
4
|
+
|
|
5
|
+
declare namespace noSQLSanitizeCore {
|
|
6
|
+
export interface StringOptions {
|
|
7
|
+
/** Trim leading/trailing whitespace. @default false */
|
|
8
|
+
trim?: boolean;
|
|
9
|
+
/** Convert to lowercase. @default false */
|
|
10
|
+
lowercase?: boolean;
|
|
11
|
+
/** Truncate strings exceeding this length. @default null */
|
|
12
|
+
maxLength?: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ArrayOptions {
|
|
16
|
+
/** Remove falsy values from arrays. @default false */
|
|
17
|
+
filterNull?: boolean;
|
|
18
|
+
/** Remove duplicate values from arrays. @default false */
|
|
19
|
+
distinct?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DebugOptions {
|
|
23
|
+
/** Enable debug logging. @default false */
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
/** Log level threshold. @default 'info' */
|
|
26
|
+
level?: 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
27
|
+
/** Log regex pattern matches. @default false */
|
|
28
|
+
logPatternMatches?: boolean;
|
|
29
|
+
/** Log sanitized values (before/after). @default false */
|
|
30
|
+
logSanitizedValues?: boolean;
|
|
31
|
+
/** Log when routes are skipped. @default false */
|
|
32
|
+
logSkippedRoutes?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Event emitted by the `onSanitize` callback when a value is changed.
|
|
37
|
+
*/
|
|
38
|
+
export interface SanitizeEvent {
|
|
39
|
+
/** The object key that was sanitized. */
|
|
40
|
+
key: string;
|
|
41
|
+
/** The original value before sanitization. */
|
|
42
|
+
originalValue: string;
|
|
43
|
+
/** The value after sanitization. */
|
|
44
|
+
sanitizedValue: string;
|
|
45
|
+
/** Path to the sanitized key (currently same as `key`). */
|
|
46
|
+
path: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* User-facing options passed to `resolveOptions()`,
|
|
51
|
+
* Express middleware, or Fastify plugin.
|
|
52
|
+
*/
|
|
53
|
+
export interface SanitizeOptions {
|
|
54
|
+
/** String to replace matched patterns with. @default '' */
|
|
55
|
+
replaceWith?: string;
|
|
56
|
+
/** Remove entire key-value pair if a pattern matches. @default false */
|
|
57
|
+
removeMatches?: boolean;
|
|
58
|
+
/** Request fields to sanitize. @default ['body', 'query'] */
|
|
59
|
+
sanitizeObjects?: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Only sanitize request body for these content types.
|
|
62
|
+
* Set to `null` to sanitize all content types.
|
|
63
|
+
* Query/params are always sanitized regardless.
|
|
64
|
+
* @default ['application/json', 'application/x-www-form-urlencoded']
|
|
65
|
+
*/
|
|
66
|
+
contentTypes?: string[] | null;
|
|
67
|
+
/** Sanitization mode. @default 'auto' */
|
|
68
|
+
mode?: 'auto' | 'manual';
|
|
69
|
+
/**
|
|
70
|
+
* Routes to skip. Supports exact strings (O(1) Set lookup)
|
|
71
|
+
* and RegExp patterns.
|
|
72
|
+
* @default []
|
|
73
|
+
*/
|
|
74
|
+
skipRoutes?: (string | RegExp)[];
|
|
75
|
+
/** Custom sanitizer function. Overrides default sanitization. @default null */
|
|
76
|
+
customSanitizer?: ((data: any, options: ResolvedOptions) => any) | null;
|
|
77
|
+
/**
|
|
78
|
+
* Callback fired when a value is sanitized. Only fires when the value changes.
|
|
79
|
+
* @default null
|
|
80
|
+
*/
|
|
81
|
+
onSanitize?: ((event: SanitizeEvent) => void) | null;
|
|
82
|
+
/** Recursively sanitize nested objects/arrays. @default true */
|
|
83
|
+
recursive?: boolean;
|
|
84
|
+
/** Remove falsy values after sanitization. @default false */
|
|
85
|
+
removeEmpty?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Maximum recursion depth for nested objects.
|
|
88
|
+
* Strings are always sanitized regardless of depth.
|
|
89
|
+
* `null` = unlimited.
|
|
90
|
+
* @default null
|
|
91
|
+
*/
|
|
92
|
+
maxDepth?: number | null;
|
|
93
|
+
/** Regex patterns to match and replace. @default [/\$/g, /control chars/g] */
|
|
94
|
+
patterns?: RegExp[];
|
|
95
|
+
/** Only allow these keys (empty = allow all). @default [] */
|
|
96
|
+
allowedKeys?: string[];
|
|
97
|
+
/** Remove these keys (empty = deny none). @default [] */
|
|
98
|
+
deniedKeys?: string[];
|
|
99
|
+
/** String transform options. */
|
|
100
|
+
stringOptions?: StringOptions;
|
|
101
|
+
/** Array transform options. */
|
|
102
|
+
arrayOptions?: ArrayOptions;
|
|
103
|
+
/** Debug logging configuration. */
|
|
104
|
+
debug?: DebugOptions;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pre-compiled skip routes split into exact (Set) and regex (Array).
|
|
109
|
+
*/
|
|
110
|
+
export interface ResolvedSkipRoutes {
|
|
111
|
+
exact: Set<string>;
|
|
112
|
+
regex: RegExp[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Options after `resolveOptions()` processing.
|
|
117
|
+
* Patterns are compiled, Sets are built, defaults are merged.
|
|
118
|
+
*/
|
|
119
|
+
export interface ResolvedOptions {
|
|
120
|
+
replaceWith: string;
|
|
121
|
+
removeMatches: boolean;
|
|
122
|
+
sanitizeObjects: string[];
|
|
123
|
+
contentTypes: Set<string> | null;
|
|
124
|
+
mode: 'auto' | 'manual';
|
|
125
|
+
skipRoutes: ResolvedSkipRoutes;
|
|
126
|
+
customSanitizer: ((data: any, options: ResolvedOptions) => any) | null;
|
|
127
|
+
onSanitize: ((event: SanitizeEvent) => void) | null;
|
|
128
|
+
recursive: boolean;
|
|
129
|
+
removeEmpty: boolean;
|
|
130
|
+
maxDepth: number | null;
|
|
131
|
+
patterns: RegExp[];
|
|
132
|
+
allowedKeys: Set<string>;
|
|
133
|
+
deniedKeys: Set<string>;
|
|
134
|
+
stringOptions: Required<StringOptions>;
|
|
135
|
+
arrayOptions: Required<ArrayOptions>;
|
|
136
|
+
debug: Required<DebugOptions>;
|
|
137
|
+
/** Pre-compiled combined regex from all patterns. */
|
|
138
|
+
_combinedPattern: RegExp;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Merge user options with defaults, pre-compile patterns,
|
|
143
|
+
* and validate configuration.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveOptions(options?: SanitizeOptions): ResolvedOptions;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sanitize a request object's body, query, and/or params in-place.
|
|
149
|
+
* Respects content-type guards and Express 5 non-writable properties.
|
|
150
|
+
*/
|
|
151
|
+
export function handleRequest(request: any, options: ResolvedOptions): void;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a request path matches any skip route.
|
|
155
|
+
* Exact strings use O(1) Set lookup, regex patterns iterate.
|
|
156
|
+
*/
|
|
157
|
+
export function shouldSkipRoute(requestPath: string, skipRoutes: ResolvedSkipRoutes, debug?: DebugOptions): boolean;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if the request's content-type is in the allowed set.
|
|
161
|
+
* Returns `true` if body should be sanitized.
|
|
162
|
+
*/
|
|
163
|
+
export function shouldSanitizeContentType(request: any, contentTypes: Set<string> | null): boolean;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a property is writable on an object or its prototype chain.
|
|
167
|
+
*/
|
|
168
|
+
export function isWritable(obj: any, prop: string): boolean;
|
|
169
|
+
|
|
170
|
+
// ── Sanitizers ────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Main dispatch — routes to appropriate sanitizer by type.
|
|
174
|
+
* @param value - Value to sanitize (string, object, array, or primitive).
|
|
175
|
+
* @param options - Resolved options.
|
|
176
|
+
* @param isValue - `true` if this is a value (not an object key).
|
|
177
|
+
* @param depth - Current recursion depth.
|
|
178
|
+
*/
|
|
179
|
+
export function sanitizeValue(value: any, options: ResolvedOptions, isValue?: boolean, depth?: number): any;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Sanitize a single string. Preserves email addresses.
|
|
183
|
+
*/
|
|
184
|
+
export function sanitizeString(str: any, options: ResolvedOptions, isValue?: boolean): any;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Sanitize all keys and values of a plain object.
|
|
188
|
+
*/
|
|
189
|
+
export function sanitizeObject(
|
|
190
|
+
obj: Record<string, any>,
|
|
191
|
+
options: ResolvedOptions,
|
|
192
|
+
depth?: number,
|
|
193
|
+
): Record<string, any>;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sanitize all elements of an array.
|
|
197
|
+
*/
|
|
198
|
+
export function sanitizeArray(arr: any[], options: ResolvedOptions, depth?: number): any[];
|
|
199
|
+
|
|
200
|
+
export function isString(value: any): value is string;
|
|
201
|
+
export function isArray(value: any): value is any[];
|
|
202
|
+
export function isPlainObject(value: any): value is Record<string, any>;
|
|
203
|
+
export function isBoolean(value: any): value is boolean;
|
|
204
|
+
export function isNumber(value: any): value is number;
|
|
205
|
+
export function isPrimitive(value: any): boolean;
|
|
206
|
+
export function isDate(value: any): value is Date;
|
|
207
|
+
export function isFunction(value: any): value is Function;
|
|
208
|
+
export function isObjectEmpty(obj: any): boolean;
|
|
209
|
+
export function isEmail(value: any): boolean;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Normalize a URL path: strip query string, trailing slashes.
|
|
213
|
+
*/
|
|
214
|
+
export function cleanUrl(url: any): string | null;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract MIME type from a Content-Type header.
|
|
218
|
+
* `"application/json; charset=utf-8"` → `"application/json"`
|
|
219
|
+
*/
|
|
220
|
+
export function extractMimeType(contentType: any): string | null;
|
|
221
|
+
|
|
222
|
+
/** Log a message at the given level. */
|
|
223
|
+
export function log(debugOpts: DebugOptions, level: string, context: string, message: string, data?: any): void;
|
|
224
|
+
|
|
225
|
+
/** Start a timing measurement. Returns a function to end timing. */
|
|
226
|
+
export function startTiming(debugOpts: DebugOptions, operation: string): () => void;
|
|
227
|
+
|
|
228
|
+
/** Validate an options object, throw on invalid config. */
|
|
229
|
+
export function validateOptions(options: Record<string, any>): void;
|
|
230
|
+
|
|
231
|
+
/** Default sanitization patterns: `$` operator + control characters. */
|
|
232
|
+
export const PATTERNS: ReadonlyArray<RegExp>;
|
|
233
|
+
/** Default options before user overrides. */
|
|
234
|
+
export const DEFAULT_OPTIONS: Readonly<SanitizeOptions>;
|
|
235
|
+
/** Numeric log level mapping. */
|
|
236
|
+
export const LOG_LEVELS: Readonly<Record<string, number>>;
|
|
237
|
+
/** ANSI color codes for log levels. */
|
|
238
|
+
export const LOG_COLORS: Readonly<Record<string, string>>;
|
|
239
|
+
|
|
240
|
+
export class NoSQLSanitizeError extends Error {
|
|
241
|
+
name: 'NoSQLSanitizeError';
|
|
242
|
+
type: string;
|
|
243
|
+
constructor(message: string, type?: string);
|
|
244
|
+
code(): string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const noSQLSanitizeCore: NoSQLSanitizeCore;
|
|
248
|
+
export { noSQLSanitizeCore as default };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
declare function noSQLSanitizeCore(
|
|
252
|
+
...params: Parameters<NoSQLSanitizeCore['resolveOptions']>
|
|
253
|
+
): ReturnType<NoSQLSanitizeCore['resolveOptions']>;
|
|
254
|
+
|
|
255
|
+
export = noSQLSanitizeCore;
|