hppx 0.1.0 → 0.1.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 +115 -26
- package/dist/index.cjs +130 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +130 -33
- package/dist/index.js.map +1 -1
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
🔐 **Superior HTTP Parameter Pollution protection middleware** for Node.js/Express, written in TypeScript. It sanitizes `req.query`, `req.body`, and `req.params`, blocks prototype-pollution keys, supports nested whitelists, multiple merge strategies, and plays nicely with stacked middlewares.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
11
|
+
- **Multiple merge strategies**: `keepFirst`, `keepLast` (default), `combine`
|
|
12
|
+
- **Enhanced security**:
|
|
13
|
+
- Blocks dangerous keys: `__proto__`, `prototype`, `constructor`
|
|
14
|
+
- Prevents null-byte injection in keys
|
|
15
|
+
- Validates key lengths to prevent DoS attacks
|
|
16
|
+
- Limits array sizes to prevent memory exhaustion
|
|
17
|
+
- **Flexible whitelisting**: Nested whitelist with dot-notation and leaf matching
|
|
18
|
+
- **Pollution tracking**: Records polluted parameters on the request (`queryPolluted`, `bodyPolluted`, `paramsPolluted`)
|
|
19
|
+
- **Multi-middleware support**: Works with multiple middlewares on different routes (whitelists applied incrementally)
|
|
20
|
+
- **DoS protection**: `maxDepth`, `maxKeys`, `maxArrayLength`, `maxKeyLength`
|
|
21
|
+
- **Performance optimized**: Path caching for improved performance
|
|
22
|
+
- **Fully typed API**: TypeScript-first with comprehensive type definitions and helper functions (`sanitize`)
|
|
18
23
|
|
|
19
24
|
## 📦 Installation
|
|
20
25
|
|
|
@@ -37,7 +42,7 @@ app.use(
|
|
|
37
42
|
whitelist: ["tags", "user.roles", "ids"],
|
|
38
43
|
mergeStrategy: "keepLast",
|
|
39
44
|
sources: ["query", "body"],
|
|
40
|
-
})
|
|
45
|
+
}),
|
|
41
46
|
);
|
|
42
47
|
|
|
43
48
|
app.get("/search", (req, res) => {
|
|
@@ -56,16 +61,33 @@ app.get("/search", (req, res) => {
|
|
|
56
61
|
|
|
57
62
|
Creates an Express-compatible middleware. Applies sanitization to each selected source and exposes `*.Polluted` objects.
|
|
58
63
|
|
|
59
|
-
Key
|
|
64
|
+
#### Key Options
|
|
65
|
+
|
|
66
|
+
**Whitelist & Strategy:**
|
|
60
67
|
|
|
61
68
|
- `whitelist?: string[]` — keys allowed as arrays; supports dot-notation; leaf matches too
|
|
62
69
|
- `mergeStrategy?: 'keepFirst'|'keepLast'|'combine'` — how to reduce arrays when not whitelisted
|
|
63
|
-
|
|
70
|
+
|
|
71
|
+
**Source Selection:**
|
|
72
|
+
|
|
73
|
+
- `sources?: Array<'query'|'body'|'params'>` — which request parts to sanitize (default: all)
|
|
64
74
|
- `checkBodyContentType?: 'urlencoded'|'any'|'none'` — when to process `req.body` (default: `urlencoded`)
|
|
65
|
-
- `excludePaths?: string[]` — exclude specific paths (supports `*` suffix)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
- `excludePaths?: string[]` — exclude specific paths (supports `*` wildcard suffix)
|
|
76
|
+
|
|
77
|
+
**Security Limits (DoS Protection):**
|
|
78
|
+
|
|
79
|
+
- `maxDepth?: number` — maximum object nesting depth (default: 20, max: 100)
|
|
80
|
+
- `maxKeys?: number` — maximum number of keys to process (default: 5000)
|
|
81
|
+
- `maxArrayLength?: number` — maximum array length (default: 1000)
|
|
82
|
+
- `maxKeyLength?: number` — maximum key string length (default: 200, max: 1000)
|
|
83
|
+
|
|
84
|
+
**Additional Options:**
|
|
85
|
+
|
|
86
|
+
- `trimValues?: boolean` — trim string values (default: false)
|
|
87
|
+
- `preserveNull?: boolean` — preserve null values (default: true)
|
|
88
|
+
- `strict?: boolean` — if pollution detected, immediately respond with 400 error
|
|
89
|
+
- `onPollutionDetected?: (req, info) => void` — callback on pollution detection
|
|
90
|
+
- `logger?: (err: Error) => void` — custom error logger
|
|
69
91
|
|
|
70
92
|
### named export: `sanitize(input, options)`
|
|
71
93
|
|
|
@@ -83,39 +105,106 @@ app.use(hppx({ strict: true }));
|
|
|
83
105
|
|
|
84
106
|
```ts
|
|
85
107
|
app.use(express.json());
|
|
86
|
-
app.use(hppx({ checkBodyContentType:
|
|
108
|
+
app.use(hppx({ checkBodyContentType: "any" }));
|
|
87
109
|
```
|
|
88
110
|
|
|
89
111
|
- Exclude specific paths (supports `*` suffix):
|
|
90
112
|
|
|
91
113
|
```ts
|
|
92
|
-
app.use(hppx({ excludePaths: [
|
|
114
|
+
app.use(hppx({ excludePaths: ["/public", "/assets*"] }));
|
|
93
115
|
```
|
|
94
116
|
|
|
95
117
|
- Use the sanitizer directly:
|
|
96
118
|
|
|
97
119
|
```ts
|
|
98
|
-
import { sanitize } from
|
|
120
|
+
import { sanitize } from "hppx";
|
|
99
121
|
|
|
100
122
|
const clean = sanitize(payload, {
|
|
101
|
-
whitelist: [
|
|
102
|
-
mergeStrategy:
|
|
123
|
+
whitelist: ["user.tags"],
|
|
124
|
+
mergeStrategy: "keepFirst",
|
|
103
125
|
});
|
|
104
126
|
```
|
|
105
127
|
|
|
106
|
-
##
|
|
128
|
+
## Security Best Practices
|
|
129
|
+
|
|
130
|
+
### Input Validation
|
|
131
|
+
|
|
132
|
+
Always combine HPP protection with additional input validation:
|
|
107
133
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
134
|
+
- Use schema validation libraries (e.g., Joi, Yup, Zod)
|
|
135
|
+
- Validate data types and ranges after sanitization
|
|
136
|
+
- Never trust user input, even after sanitization
|
|
137
|
+
|
|
138
|
+
### Configuration Recommendations
|
|
139
|
+
|
|
140
|
+
For production environments, consider these settings:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
app.use(
|
|
144
|
+
hppx({
|
|
145
|
+
maxDepth: 10, // Lower depth for typical use cases
|
|
146
|
+
maxKeys: 1000, // Reasonable limit for most requests
|
|
147
|
+
maxArrayLength: 100, // Prevent large array attacks
|
|
148
|
+
maxKeyLength: 100, // Shorter keys for most applications
|
|
149
|
+
strict: true, // Return 400 on pollution attempts
|
|
150
|
+
onPollutionDetected: (req, info) => {
|
|
151
|
+
// Log security events for monitoring
|
|
152
|
+
securityLogger.warn("HPP detected", {
|
|
153
|
+
ip: req.ip,
|
|
154
|
+
path: req.path,
|
|
155
|
+
pollutedKeys: info.pollutedKeys,
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### What HPP Protects Against
|
|
163
|
+
|
|
164
|
+
- **Parameter pollution**: Duplicate parameters causing unexpected behavior
|
|
165
|
+
- **Prototype pollution**: Attacks via `__proto__`, `constructor`, `prototype`
|
|
166
|
+
- **DoS attacks**: Excessive nesting, too many keys, huge arrays
|
|
167
|
+
- **Null-byte injection**: Keys containing null characters (`\u0000`)
|
|
168
|
+
|
|
169
|
+
### What HPP Does NOT Protect Against
|
|
170
|
+
|
|
171
|
+
HPP is not a complete security solution. You still need:
|
|
172
|
+
|
|
173
|
+
- **SQL injection protection**: Use parameterized queries
|
|
174
|
+
- **XSS protection**: Sanitize output, use CSP headers
|
|
175
|
+
- **CSRF protection**: Use CSRF tokens
|
|
176
|
+
- **Authentication/Authorization**: Validate user permissions
|
|
177
|
+
- **Rate limiting**: Prevent brute-force attacks
|
|
111
178
|
|
|
112
179
|
## 📄 License
|
|
113
180
|
|
|
114
181
|
MIT License - see [LICENSE](LICENSE) file for details.
|
|
115
182
|
|
|
183
|
+
## Changelog
|
|
184
|
+
|
|
185
|
+
### v0.1.1 (Security & Performance Update)
|
|
186
|
+
|
|
187
|
+
- **Security Enhancements:**
|
|
188
|
+
- Added `maxArrayLength` to prevent memory exhaustion attacks
|
|
189
|
+
- Added `maxKeyLength` to prevent long key DoS attacks
|
|
190
|
+
- Enhanced prototype pollution protection in nested operations
|
|
191
|
+
- Fixed validation of malformed keys (null bytes, bracket/dot-only keys)
|
|
192
|
+
- Added comprehensive options validation with helpful error messages
|
|
193
|
+
- **Bug Fixes:**
|
|
194
|
+
- Fixed `onPollutionDetected` callback receiving correct source information
|
|
195
|
+
- Improved error handling with proper error propagation
|
|
196
|
+
- **Performance:**
|
|
197
|
+
- Added path caching for faster whitelist checks
|
|
198
|
+
- Added path segment caching to reduce parsing overhead
|
|
199
|
+
- Optimized repeated sanitization operations
|
|
200
|
+
- **Developer Experience:**
|
|
201
|
+
- Improved TypeScript types and removed unnecessary `any` types
|
|
202
|
+
- Enhanced error messages and logging
|
|
203
|
+
- Added comprehensive test suite for security features
|
|
204
|
+
|
|
116
205
|
## 🔗 Links
|
|
117
206
|
|
|
118
|
-
- [NPM Package](https://www.npmjs.com/package
|
|
207
|
+
- [NPM Package](https://www.npmjs.com/package/hppx)
|
|
119
208
|
- [GitHub Repository](https://github.com/Hiprax/hppx)
|
|
120
209
|
- [Issue Tracker](https://github.com/Hiprax/hppx/issues)
|
|
121
210
|
|
package/dist/index.cjs
CHANGED
|
@@ -35,23 +35,33 @@ function isPlainObject(value) {
|
|
|
35
35
|
const proto = Object.getPrototypeOf(value);
|
|
36
36
|
return proto === Object.prototype || proto === null;
|
|
37
37
|
}
|
|
38
|
-
function sanitizeKey(key) {
|
|
38
|
+
function sanitizeKey(key, maxKeyLength) {
|
|
39
39
|
if (typeof key !== "string") return null;
|
|
40
40
|
if (DANGEROUS_KEYS.has(key)) return null;
|
|
41
41
|
if (key.includes("\0")) return null;
|
|
42
|
+
const maxLen = maxKeyLength ?? 200;
|
|
43
|
+
if (key.length > maxLen) return null;
|
|
44
|
+
if (key.length > 1 && /^[.\[\]]+$/.test(key)) return null;
|
|
42
45
|
return key;
|
|
43
46
|
}
|
|
47
|
+
var pathSegmentCache = /* @__PURE__ */ new Map();
|
|
44
48
|
function parsePathSegments(key) {
|
|
49
|
+
const cached = pathSegmentCache.get(key);
|
|
50
|
+
if (cached) return cached;
|
|
45
51
|
const dotted = key.replace(/\]/g, "").replace(/\[/g, ".");
|
|
46
|
-
|
|
52
|
+
const result = dotted.split(".").filter((s) => s.length > 0);
|
|
53
|
+
if (pathSegmentCache.size < 500) {
|
|
54
|
+
pathSegmentCache.set(key, result);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
47
57
|
}
|
|
48
|
-
function expandObjectPaths(obj) {
|
|
58
|
+
function expandObjectPaths(obj, maxKeyLength) {
|
|
49
59
|
const result = {};
|
|
50
60
|
for (const rawKey of Object.keys(obj)) {
|
|
51
|
-
const safeKey = sanitizeKey(rawKey);
|
|
61
|
+
const safeKey = sanitizeKey(rawKey, maxKeyLength);
|
|
52
62
|
if (!safeKey) continue;
|
|
53
63
|
const value = obj[rawKey];
|
|
54
|
-
const expandedValue = isPlainObject(value) ? expandObjectPaths(value) : value;
|
|
64
|
+
const expandedValue = isPlainObject(value) ? expandObjectPaths(value, maxKeyLength) : value;
|
|
55
65
|
if (safeKey.includes(".") || safeKey.includes("[")) {
|
|
56
66
|
const segments = parsePathSegments(safeKey);
|
|
57
67
|
if (segments.length > 0) {
|
|
@@ -85,15 +95,17 @@ function setReqPropertySafe(target, key, value) {
|
|
|
85
95
|
} catch (_) {
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
function safeDeepClone(input) {
|
|
98
|
+
function safeDeepClone(input, maxKeyLength, maxArrayLength) {
|
|
89
99
|
if (Array.isArray(input)) {
|
|
90
|
-
|
|
100
|
+
const limit = maxArrayLength ?? 1e3;
|
|
101
|
+
const limited = input.slice(0, limit);
|
|
102
|
+
return limited.map((v) => safeDeepClone(v, maxKeyLength, maxArrayLength));
|
|
91
103
|
}
|
|
92
104
|
if (isPlainObject(input)) {
|
|
93
105
|
const out = {};
|
|
94
106
|
for (const k of Object.keys(input)) {
|
|
95
|
-
if (!sanitizeKey(k)) continue;
|
|
96
|
-
out[k] = safeDeepClone(input[k]);
|
|
107
|
+
if (!sanitizeKey(k, maxKeyLength)) continue;
|
|
108
|
+
out[k] = safeDeepClone(input[k], maxKeyLength, maxArrayLength);
|
|
97
109
|
}
|
|
98
110
|
return out;
|
|
99
111
|
}
|
|
@@ -139,19 +151,32 @@ function normalizeWhitelist(whitelist) {
|
|
|
139
151
|
function buildWhitelistHelpers(whitelist) {
|
|
140
152
|
const exact = new Set(whitelist);
|
|
141
153
|
const prefixes = whitelist.filter((w) => w.length > 0);
|
|
154
|
+
const pathCache = /* @__PURE__ */ new Map();
|
|
142
155
|
return {
|
|
143
156
|
exact,
|
|
144
157
|
prefixes,
|
|
145
158
|
isWhitelistedPath(pathParts) {
|
|
146
159
|
if (pathParts.length === 0) return false;
|
|
147
160
|
const full = pathParts.join(".");
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
const cached = pathCache.get(full);
|
|
162
|
+
if (cached !== void 0) return cached;
|
|
163
|
+
let result = false;
|
|
164
|
+
if (exact.has(full)) {
|
|
165
|
+
result = true;
|
|
166
|
+
} else if (exact.has(pathParts[pathParts.length - 1])) {
|
|
167
|
+
result = true;
|
|
168
|
+
} else {
|
|
169
|
+
for (const p of prefixes) {
|
|
170
|
+
if (full === p || full.startsWith(p + ".")) {
|
|
171
|
+
result = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (pathCache.size < 1e3) {
|
|
177
|
+
pathCache.set(full, result);
|
|
153
178
|
}
|
|
154
|
-
return
|
|
179
|
+
return result;
|
|
155
180
|
}
|
|
156
181
|
};
|
|
157
182
|
}
|
|
@@ -162,19 +187,23 @@ function setIn(target, path, value) {
|
|
|
162
187
|
let cur = target;
|
|
163
188
|
for (let i = 0; i < path.length - 1; i++) {
|
|
164
189
|
const k = path[i];
|
|
165
|
-
if (
|
|
190
|
+
if (DANGEROUS_KEYS.has(k)) return;
|
|
191
|
+
if (!isPlainObject(cur[k])) {
|
|
192
|
+
cur[k] = {};
|
|
193
|
+
}
|
|
166
194
|
cur = cur[k];
|
|
167
195
|
}
|
|
168
196
|
const lastKey = path[path.length - 1];
|
|
197
|
+
if (DANGEROUS_KEYS.has(lastKey)) return;
|
|
169
198
|
cur[lastKey] = value;
|
|
170
199
|
}
|
|
171
200
|
function moveWhitelistedFromPolluted(reqPart, polluted, isWhitelisted) {
|
|
172
|
-
function walk(node, path = []
|
|
201
|
+
function walk(node, path = []) {
|
|
173
202
|
for (const k of Object.keys(node)) {
|
|
174
203
|
const v = node[k];
|
|
175
204
|
const curPath = [...path, k];
|
|
176
205
|
if (isPlainObject(v)) {
|
|
177
|
-
walk(v, curPath
|
|
206
|
+
walk(v, curPath);
|
|
178
207
|
if (Object.keys(v).length === 0) {
|
|
179
208
|
delete node[k];
|
|
180
209
|
}
|
|
@@ -198,11 +227,13 @@ function detectAndReduce(input, opts) {
|
|
|
198
227
|
function processNode(node, path = [], depth = 0) {
|
|
199
228
|
if (node === null || node === void 0) return opts.preserveNull ? node : node;
|
|
200
229
|
if (Array.isArray(node)) {
|
|
201
|
-
const
|
|
230
|
+
const limit = opts.maxArrayLength ?? 1e3;
|
|
231
|
+
const limitedNode = node.slice(0, limit);
|
|
232
|
+
const mapped = limitedNode.map((v) => processNode(v, path, depth));
|
|
202
233
|
if (opts.mergeStrategy === "combine") {
|
|
203
234
|
return mergeValues(mapped, "combine");
|
|
204
235
|
}
|
|
205
|
-
setIn(polluted, path, safeDeepClone(
|
|
236
|
+
setIn(polluted, path, safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength));
|
|
206
237
|
pollutedKeys.push(path.join("."));
|
|
207
238
|
const reduced = mergeValues(mapped, opts.mergeStrategy);
|
|
208
239
|
return reduced;
|
|
@@ -216,7 +247,7 @@ function detectAndReduce(input, opts) {
|
|
|
216
247
|
if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {
|
|
217
248
|
throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);
|
|
218
249
|
}
|
|
219
|
-
const safeKey = sanitizeKey(rawKey);
|
|
250
|
+
const safeKey = sanitizeKey(rawKey, opts.maxKeyLength);
|
|
220
251
|
if (!safeKey) continue;
|
|
221
252
|
const child = node[rawKey];
|
|
222
253
|
const childPath = path.concat([safeKey]);
|
|
@@ -228,18 +259,20 @@ function detectAndReduce(input, opts) {
|
|
|
228
259
|
}
|
|
229
260
|
return node;
|
|
230
261
|
}
|
|
231
|
-
const cloned = safeDeepClone(input);
|
|
262
|
+
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);
|
|
232
263
|
const cleaned = processNode(cloned, [], 0);
|
|
233
264
|
return { cleaned, pollutedTree: polluted, pollutedKeys };
|
|
234
265
|
}
|
|
235
266
|
function sanitize(input, options = {}) {
|
|
236
|
-
const
|
|
267
|
+
const maxKeyLength = options.maxKeyLength ?? 200;
|
|
268
|
+
const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength) : input;
|
|
237
269
|
const whitelist = normalizeWhitelist(options.whitelist);
|
|
238
270
|
const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
|
|
239
271
|
const {
|
|
240
272
|
mergeStrategy = DEFAULT_STRATEGY,
|
|
241
273
|
maxDepth = 20,
|
|
242
274
|
maxKeys = 5e3,
|
|
275
|
+
maxArrayLength = 1e3,
|
|
243
276
|
trimValues = false,
|
|
244
277
|
preserveNull = true
|
|
245
278
|
} = options;
|
|
@@ -247,13 +280,49 @@ function sanitize(input, options = {}) {
|
|
|
247
280
|
mergeStrategy,
|
|
248
281
|
maxDepth,
|
|
249
282
|
maxKeys,
|
|
283
|
+
maxArrayLength,
|
|
284
|
+
maxKeyLength,
|
|
250
285
|
trimValues,
|
|
251
286
|
preserveNull
|
|
252
287
|
});
|
|
253
288
|
moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
|
|
254
289
|
return cleaned;
|
|
255
290
|
}
|
|
291
|
+
function validateOptions(options) {
|
|
292
|
+
if (options.maxDepth !== void 0 && (typeof options.maxDepth !== "number" || options.maxDepth < 1 || options.maxDepth > 100)) {
|
|
293
|
+
throw new TypeError("maxDepth must be a number between 1 and 100");
|
|
294
|
+
}
|
|
295
|
+
if (options.maxKeys !== void 0 && (typeof options.maxKeys !== "number" || options.maxKeys < 1)) {
|
|
296
|
+
throw new TypeError("maxKeys must be a positive number");
|
|
297
|
+
}
|
|
298
|
+
if (options.maxArrayLength !== void 0 && (typeof options.maxArrayLength !== "number" || options.maxArrayLength < 1)) {
|
|
299
|
+
throw new TypeError("maxArrayLength must be a positive number");
|
|
300
|
+
}
|
|
301
|
+
if (options.maxKeyLength !== void 0 && (typeof options.maxKeyLength !== "number" || options.maxKeyLength < 1 || options.maxKeyLength > 1e3)) {
|
|
302
|
+
throw new TypeError("maxKeyLength must be a number between 1 and 1000");
|
|
303
|
+
}
|
|
304
|
+
if (options.mergeStrategy !== void 0 && !["keepFirst", "keepLast", "combine"].includes(options.mergeStrategy)) {
|
|
305
|
+
throw new TypeError("mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'");
|
|
306
|
+
}
|
|
307
|
+
if (options.sources !== void 0 && !Array.isArray(options.sources)) {
|
|
308
|
+
throw new TypeError("sources must be an array");
|
|
309
|
+
}
|
|
310
|
+
if (options.sources !== void 0) {
|
|
311
|
+
for (const source of options.sources) {
|
|
312
|
+
if (!["query", "body", "params"].includes(source)) {
|
|
313
|
+
throw new TypeError("sources must only contain 'query', 'body', or 'params'");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (options.checkBodyContentType !== void 0 && !["urlencoded", "any", "none"].includes(options.checkBodyContentType)) {
|
|
318
|
+
throw new TypeError("checkBodyContentType must be 'urlencoded', 'any', or 'none'");
|
|
319
|
+
}
|
|
320
|
+
if (options.excludePaths !== void 0 && !Array.isArray(options.excludePaths)) {
|
|
321
|
+
throw new TypeError("excludePaths must be an array");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
256
324
|
function hppx(options = {}) {
|
|
325
|
+
validateOptions(options);
|
|
257
326
|
const {
|
|
258
327
|
whitelist = [],
|
|
259
328
|
mergeStrategy = DEFAULT_STRATEGY,
|
|
@@ -262,6 +331,8 @@ function hppx(options = {}) {
|
|
|
262
331
|
excludePaths = [],
|
|
263
332
|
maxDepth = 20,
|
|
264
333
|
maxKeys = 5e3,
|
|
334
|
+
maxArrayLength = 1e3,
|
|
335
|
+
maxKeyLength = 200,
|
|
265
336
|
trimValues = false,
|
|
266
337
|
preserveNull = true,
|
|
267
338
|
strict = false,
|
|
@@ -284,7 +355,7 @@ function hppx(options = {}) {
|
|
|
284
355
|
}
|
|
285
356
|
const part = req[source];
|
|
286
357
|
if (!isPlainObject(part)) continue;
|
|
287
|
-
const expandedPart = expandObjectPaths(part);
|
|
358
|
+
const expandedPart = expandObjectPaths(part, maxKeyLength);
|
|
288
359
|
const pollutedKey = `${source}Polluted`;
|
|
289
360
|
const processedKey = `__hppxProcessed_${source}`;
|
|
290
361
|
const hasProcessedBefore = Boolean(req[processedKey]);
|
|
@@ -293,28 +364,49 @@ function hppx(options = {}) {
|
|
|
293
364
|
mergeStrategy,
|
|
294
365
|
maxDepth,
|
|
295
366
|
maxKeys,
|
|
367
|
+
maxArrayLength,
|
|
368
|
+
maxKeyLength,
|
|
296
369
|
trimValues,
|
|
297
370
|
preserveNull
|
|
298
371
|
});
|
|
299
372
|
setReqPropertySafe(req, source, cleaned);
|
|
300
373
|
setReqPropertySafe(req, pollutedKey, pollutedTree);
|
|
301
374
|
req[processedKey] = true;
|
|
302
|
-
|
|
375
|
+
const sourceData = req[source];
|
|
376
|
+
const pollutedData = req[pollutedKey];
|
|
377
|
+
if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {
|
|
378
|
+
moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);
|
|
379
|
+
}
|
|
303
380
|
if (pollutedKeys.length > 0) {
|
|
304
381
|
anyPollutionDetected = true;
|
|
305
382
|
for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);
|
|
306
383
|
}
|
|
307
384
|
} else {
|
|
308
|
-
|
|
385
|
+
const sourceData = req[source];
|
|
386
|
+
const pollutedData = req[pollutedKey];
|
|
387
|
+
if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {
|
|
388
|
+
moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);
|
|
389
|
+
}
|
|
309
390
|
}
|
|
310
391
|
}
|
|
311
392
|
if (anyPollutionDetected) {
|
|
312
393
|
if (onPollutionDetected) {
|
|
313
394
|
try {
|
|
314
|
-
|
|
315
|
-
source
|
|
316
|
-
|
|
317
|
-
|
|
395
|
+
for (const source of sources) {
|
|
396
|
+
const pollutedKey = `${source}Polluted`;
|
|
397
|
+
const pollutedData = req[pollutedKey];
|
|
398
|
+
if (pollutedData && Object.keys(pollutedData).length > 0) {
|
|
399
|
+
const sourcePollutedKeys = allPollutedKeys.filter(
|
|
400
|
+
(k) => k.startsWith(`${source}.`)
|
|
401
|
+
);
|
|
402
|
+
if (sourcePollutedKeys.length > 0) {
|
|
403
|
+
onPollutionDetected(req, {
|
|
404
|
+
source,
|
|
405
|
+
pollutedKeys: sourcePollutedKeys
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
318
410
|
} catch (_) {
|
|
319
411
|
}
|
|
320
412
|
}
|
|
@@ -329,13 +421,18 @@ function hppx(options = {}) {
|
|
|
329
421
|
}
|
|
330
422
|
return next();
|
|
331
423
|
} catch (err) {
|
|
424
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
332
425
|
if (logger) {
|
|
333
426
|
try {
|
|
334
|
-
logger(
|
|
335
|
-
} catch (
|
|
427
|
+
logger(error);
|
|
428
|
+
} catch (logErr) {
|
|
429
|
+
if (process.env.NODE_ENV !== "production") {
|
|
430
|
+
console.error("[hppx] Logger failed:", logErr);
|
|
431
|
+
console.error("[hppx] Original error:", error);
|
|
432
|
+
}
|
|
336
433
|
}
|
|
337
434
|
}
|
|
338
|
-
return next(
|
|
435
|
+
return next(error);
|
|
339
436
|
}
|
|
340
437
|
};
|
|
341
438
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (req: any, info: { source: RequestSource; pollutedKeys: string[] }) => void;\r\n logger?: (err: unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n return key;\r\n}\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n return dotted.split(\".\").filter((s) => s.length > 0);\r\n}\r\n\r\nfunction expandObjectPaths(obj: Record<string, unknown>): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const value = (obj as any)[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: any, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T): T {\r\n if (Array.isArray(input)) {\r\n return input.map((v) => safeDeepClone(v)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k]);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n if (exact.has(full)) return true;\r\n // leaf match\r\n const leaf = pathParts[pathParts.length - 1]!;\r\n if (exact.has(leaf)) return true;\r\n // prefix match (treat any listed segment as prefix of a subtree)\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) return true;\r\n }\r\n return false;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: any = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n if (!isPlainObject(cur[k])) cur[k] = {};\r\n cur = cur[k];\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(\r\n node: Record<string, unknown>,\r\n path: string[] = [],\r\n parent?: Record<string, unknown>,\r\n ) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath, node);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete (node as any)[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete (node as any)[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<SanitizeOptions, \"mergeStrategy\" | \"maxDepth\" | \"maxKeys\" | \"trimValues\" | \"preserveNull\">\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n const mapped = node.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(node));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: any) => void;\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */ if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean((req as any)[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n (req as any)[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n onPollutionDetected(req, {\r\n source: \"query\",\r\n pollutedKeys: allPollutedKeys,\r\n });\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n if (logger) {\r\n try {\r\n logger(err);\r\n } catch (_) {\r\n /* noop */\r\n }\r\n }\r\n return next(err);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n maxArrayLength?: number;\r\n maxKeyLength?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (\r\n req: Record<string, unknown>,\r\n info: { source: RequestSource; pollutedKeys: string[] },\r\n ) => void;\r\n logger?: (err: Error | unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string, maxKeyLength?: number): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n // Prevent excessively long keys that could cause DoS\r\n const maxLen = maxKeyLength ?? 200;\r\n if (key.length > maxLen) return null;\r\n // Prevent keys that are only dots or brackets (malformed) - but allow single dot as it's valid\r\n if (key.length > 1 && /^[.\\[\\]]+$/.test(key)) return null;\r\n return key;\r\n}\r\n\r\n// Cache for parsed path segments to improve performance\r\nconst pathSegmentCache = new Map<string, string[]>();\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Check cache first\r\n const cached = pathSegmentCache.get(key);\r\n if (cached) return cached;\r\n\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n const result = dotted.split(\".\").filter((s) => s.length > 0);\r\n\r\n // Cache the result (limit cache size)\r\n if (pathSegmentCache.size < 500) {\r\n pathSegmentCache.set(key, result);\r\n }\r\n\r\n return result;\r\n}\r\n\r\nfunction expandObjectPaths(\r\n obj: Record<string, unknown>,\r\n maxKeyLength?: number,\r\n): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey, maxKeyLength);\r\n if (!safeKey) continue;\r\n const value = obj[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>, maxKeyLength)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: Record<string, unknown>, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T, maxKeyLength?: number, maxArrayLength?: number): T {\r\n if (Array.isArray(input)) {\r\n // Limit array length to prevent memory exhaustion\r\n const limit = maxArrayLength ?? 1000;\r\n const limited = input.slice(0, limit);\r\n return limited.map((v) => safeDeepClone(v, maxKeyLength, maxArrayLength)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k, maxKeyLength)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k], maxKeyLength, maxArrayLength);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n // Pre-build a cache for commonly checked paths for performance\r\n const pathCache = new Map<string, boolean>();\r\n\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n\r\n // Check cache first for performance\r\n const cached = pathCache.get(full);\r\n if (cached !== undefined) return cached;\r\n\r\n let result = false;\r\n\r\n // Exact match\r\n if (exact.has(full)) {\r\n result = true;\r\n }\r\n // Leaf match\r\n else if (exact.has(pathParts[pathParts.length - 1]!)) {\r\n result = true;\r\n }\r\n // Prefix match (treat any listed segment as prefix of a subtree)\r\n else {\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) {\r\n result = true;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // Cache the result (limit cache size to prevent memory issues)\r\n if (pathCache.size < 1000) {\r\n pathCache.set(full, result);\r\n }\r\n\r\n return result;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: Record<string, unknown> = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n // Additional prototype pollution protection\r\n if (DANGEROUS_KEYS.has(k)) return;\r\n if (!isPlainObject(cur[k])) {\r\n // Create a new plain object to avoid pollution\r\n cur[k] = {};\r\n }\r\n cur = cur[k] as Record<string, unknown>;\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n // Final check on the last key\r\n if (DANGEROUS_KEYS.has(lastKey)) return;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(node: Record<string, unknown>, path: string[] = []) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete node[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete node[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<\r\n SanitizeOptions,\r\n | \"mergeStrategy\"\r\n | \"maxDepth\"\r\n | \"maxKeys\"\r\n | \"maxArrayLength\"\r\n | \"maxKeyLength\"\r\n | \"trimValues\"\r\n | \"preserveNull\"\r\n >\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n // Limit array length to prevent DoS\r\n const limit = opts.maxArrayLength ?? 1000;\r\n const limitedNode = node.slice(0, limit);\r\n\r\n const mapped = limitedNode.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey, opts.maxKeyLength);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const maxKeyLength = options.maxKeyLength ?? 200;\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n maxArrayLength = 1000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n maxArrayLength,\r\n maxKeyLength,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: unknown) => void;\r\n\r\nfunction validateOptions(options: HppxOptions): void {\r\n if (\r\n options.maxDepth !== undefined &&\r\n (typeof options.maxDepth !== \"number\" || options.maxDepth < 1 || options.maxDepth > 100)\r\n ) {\r\n throw new TypeError(\"maxDepth must be a number between 1 and 100\");\r\n }\r\n if (\r\n options.maxKeys !== undefined &&\r\n (typeof options.maxKeys !== \"number\" || options.maxKeys < 1)\r\n ) {\r\n throw new TypeError(\"maxKeys must be a positive number\");\r\n }\r\n if (\r\n options.maxArrayLength !== undefined &&\r\n (typeof options.maxArrayLength !== \"number\" || options.maxArrayLength < 1)\r\n ) {\r\n throw new TypeError(\"maxArrayLength must be a positive number\");\r\n }\r\n if (\r\n options.maxKeyLength !== undefined &&\r\n (typeof options.maxKeyLength !== \"number\" ||\r\n options.maxKeyLength < 1 ||\r\n options.maxKeyLength > 1000)\r\n ) {\r\n throw new TypeError(\"maxKeyLength must be a number between 1 and 1000\");\r\n }\r\n if (\r\n options.mergeStrategy !== undefined &&\r\n ![\"keepFirst\", \"keepLast\", \"combine\"].includes(options.mergeStrategy)\r\n ) {\r\n throw new TypeError(\"mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'\");\r\n }\r\n if (options.sources !== undefined && !Array.isArray(options.sources)) {\r\n throw new TypeError(\"sources must be an array\");\r\n }\r\n if (options.sources !== undefined) {\r\n for (const source of options.sources) {\r\n if (![\"query\", \"body\", \"params\"].includes(source)) {\r\n throw new TypeError(\"sources must only contain 'query', 'body', or 'params'\");\r\n }\r\n }\r\n }\r\n if (\r\n options.checkBodyContentType !== undefined &&\r\n ![\"urlencoded\", \"any\", \"none\"].includes(options.checkBodyContentType)\r\n ) {\r\n throw new TypeError(\"checkBodyContentType must be 'urlencoded', 'any', or 'none'\");\r\n }\r\n if (options.excludePaths !== undefined && !Array.isArray(options.excludePaths)) {\r\n throw new TypeError(\"excludePaths must be an array\");\r\n }\r\n}\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n // Validate options on middleware creation\r\n validateOptions(options);\r\n\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n maxArrayLength = 1000,\r\n maxKeyLength = 200,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */\r\n if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part, maxKeyLength);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean(req[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n maxArrayLength,\r\n maxKeyLength,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n req[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n const sourceData = req[source];\r\n const pollutedData = req[pollutedKey];\r\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\r\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\r\n }\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n const sourceData = req[source];\r\n const pollutedData = req[pollutedKey];\r\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\r\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\r\n }\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n // Determine which sources had pollution\r\n for (const source of sources) {\r\n const pollutedKey = `${source}Polluted`;\r\n const pollutedData = req[pollutedKey];\r\n if (pollutedData && Object.keys(pollutedData).length > 0) {\r\n const sourcePollutedKeys = allPollutedKeys.filter((k) =>\r\n k.startsWith(`${source}.`),\r\n );\r\n if (sourcePollutedKeys.length > 0) {\r\n onPollutionDetected(req, {\r\n source: source,\r\n pollutedKeys: sourcePollutedKeys,\r\n });\r\n }\r\n }\r\n }\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n // Enhanced error handling with detailed logging\r\n const error = err instanceof Error ? err : new Error(String(err));\r\n\r\n if (logger) {\r\n try {\r\n logger(error);\r\n } catch (logErr) {\r\n // If custom logger fails, use console.error as fallback in development\r\n if (process.env.NODE_ENV !== \"production\") {\r\n console.error(\"[hppx] Logger failed:\", logErr);\r\n console.error(\"[hppx] Original error:\", error);\r\n }\r\n }\r\n }\r\n\r\n // Pass error to next middleware for proper error handling\r\n return next(error);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -15,6 +15,8 @@ interface SanitizeOptions {
|
|
|
15
15
|
mergeStrategy?: MergeStrategy;
|
|
16
16
|
maxDepth?: number;
|
|
17
17
|
maxKeys?: number;
|
|
18
|
+
maxArrayLength?: number;
|
|
19
|
+
maxKeyLength?: number;
|
|
18
20
|
trimValues?: boolean;
|
|
19
21
|
preserveNull?: boolean;
|
|
20
22
|
}
|
|
@@ -24,11 +26,11 @@ interface HppxOptions extends SanitizeOptions {
|
|
|
24
26
|
checkBodyContentType?: "urlencoded" | "any" | "none";
|
|
25
27
|
excludePaths?: string[];
|
|
26
28
|
strict?: boolean;
|
|
27
|
-
onPollutionDetected?: (req:
|
|
29
|
+
onPollutionDetected?: (req: Record<string, unknown>, info: {
|
|
28
30
|
source: RequestSource;
|
|
29
31
|
pollutedKeys: string[];
|
|
30
32
|
}) => void;
|
|
31
|
-
logger?: (err: unknown) => void;
|
|
33
|
+
logger?: (err: Error | unknown) => void;
|
|
32
34
|
}
|
|
33
35
|
interface SanitizedResult<T> {
|
|
34
36
|
cleaned: T;
|
|
@@ -39,7 +41,7 @@ declare const DEFAULT_SOURCES: RequestSource[];
|
|
|
39
41
|
declare const DEFAULT_STRATEGY: MergeStrategy;
|
|
40
42
|
declare const DANGEROUS_KEYS: Set<string>;
|
|
41
43
|
declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
|
|
42
|
-
type ExpressLikeNext = (err?:
|
|
44
|
+
type ExpressLikeNext = (err?: unknown) => void;
|
|
43
45
|
declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
|
|
44
46
|
|
|
45
47
|
export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, hppx as default, sanitize };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ interface SanitizeOptions {
|
|
|
15
15
|
mergeStrategy?: MergeStrategy;
|
|
16
16
|
maxDepth?: number;
|
|
17
17
|
maxKeys?: number;
|
|
18
|
+
maxArrayLength?: number;
|
|
19
|
+
maxKeyLength?: number;
|
|
18
20
|
trimValues?: boolean;
|
|
19
21
|
preserveNull?: boolean;
|
|
20
22
|
}
|
|
@@ -24,11 +26,11 @@ interface HppxOptions extends SanitizeOptions {
|
|
|
24
26
|
checkBodyContentType?: "urlencoded" | "any" | "none";
|
|
25
27
|
excludePaths?: string[];
|
|
26
28
|
strict?: boolean;
|
|
27
|
-
onPollutionDetected?: (req:
|
|
29
|
+
onPollutionDetected?: (req: Record<string, unknown>, info: {
|
|
28
30
|
source: RequestSource;
|
|
29
31
|
pollutedKeys: string[];
|
|
30
32
|
}) => void;
|
|
31
|
-
logger?: (err: unknown) => void;
|
|
33
|
+
logger?: (err: Error | unknown) => void;
|
|
32
34
|
}
|
|
33
35
|
interface SanitizedResult<T> {
|
|
34
36
|
cleaned: T;
|
|
@@ -39,7 +41,7 @@ declare const DEFAULT_SOURCES: RequestSource[];
|
|
|
39
41
|
declare const DEFAULT_STRATEGY: MergeStrategy;
|
|
40
42
|
declare const DANGEROUS_KEYS: Set<string>;
|
|
41
43
|
declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
|
|
42
|
-
type ExpressLikeNext = (err?:
|
|
44
|
+
type ExpressLikeNext = (err?: unknown) => void;
|
|
43
45
|
declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
|
|
44
46
|
|
|
45
47
|
export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, hppx as default, sanitize };
|
package/dist/index.js
CHANGED
|
@@ -7,23 +7,33 @@ function isPlainObject(value) {
|
|
|
7
7
|
const proto = Object.getPrototypeOf(value);
|
|
8
8
|
return proto === Object.prototype || proto === null;
|
|
9
9
|
}
|
|
10
|
-
function sanitizeKey(key) {
|
|
10
|
+
function sanitizeKey(key, maxKeyLength) {
|
|
11
11
|
if (typeof key !== "string") return null;
|
|
12
12
|
if (DANGEROUS_KEYS.has(key)) return null;
|
|
13
13
|
if (key.includes("\0")) return null;
|
|
14
|
+
const maxLen = maxKeyLength ?? 200;
|
|
15
|
+
if (key.length > maxLen) return null;
|
|
16
|
+
if (key.length > 1 && /^[.\[\]]+$/.test(key)) return null;
|
|
14
17
|
return key;
|
|
15
18
|
}
|
|
19
|
+
var pathSegmentCache = /* @__PURE__ */ new Map();
|
|
16
20
|
function parsePathSegments(key) {
|
|
21
|
+
const cached = pathSegmentCache.get(key);
|
|
22
|
+
if (cached) return cached;
|
|
17
23
|
const dotted = key.replace(/\]/g, "").replace(/\[/g, ".");
|
|
18
|
-
|
|
24
|
+
const result = dotted.split(".").filter((s) => s.length > 0);
|
|
25
|
+
if (pathSegmentCache.size < 500) {
|
|
26
|
+
pathSegmentCache.set(key, result);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
19
29
|
}
|
|
20
|
-
function expandObjectPaths(obj) {
|
|
30
|
+
function expandObjectPaths(obj, maxKeyLength) {
|
|
21
31
|
const result = {};
|
|
22
32
|
for (const rawKey of Object.keys(obj)) {
|
|
23
|
-
const safeKey = sanitizeKey(rawKey);
|
|
33
|
+
const safeKey = sanitizeKey(rawKey, maxKeyLength);
|
|
24
34
|
if (!safeKey) continue;
|
|
25
35
|
const value = obj[rawKey];
|
|
26
|
-
const expandedValue = isPlainObject(value) ? expandObjectPaths(value) : value;
|
|
36
|
+
const expandedValue = isPlainObject(value) ? expandObjectPaths(value, maxKeyLength) : value;
|
|
27
37
|
if (safeKey.includes(".") || safeKey.includes("[")) {
|
|
28
38
|
const segments = parsePathSegments(safeKey);
|
|
29
39
|
if (segments.length > 0) {
|
|
@@ -57,15 +67,17 @@ function setReqPropertySafe(target, key, value) {
|
|
|
57
67
|
} catch (_) {
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
|
-
function safeDeepClone(input) {
|
|
70
|
+
function safeDeepClone(input, maxKeyLength, maxArrayLength) {
|
|
61
71
|
if (Array.isArray(input)) {
|
|
62
|
-
|
|
72
|
+
const limit = maxArrayLength ?? 1e3;
|
|
73
|
+
const limited = input.slice(0, limit);
|
|
74
|
+
return limited.map((v) => safeDeepClone(v, maxKeyLength, maxArrayLength));
|
|
63
75
|
}
|
|
64
76
|
if (isPlainObject(input)) {
|
|
65
77
|
const out = {};
|
|
66
78
|
for (const k of Object.keys(input)) {
|
|
67
|
-
if (!sanitizeKey(k)) continue;
|
|
68
|
-
out[k] = safeDeepClone(input[k]);
|
|
79
|
+
if (!sanitizeKey(k, maxKeyLength)) continue;
|
|
80
|
+
out[k] = safeDeepClone(input[k], maxKeyLength, maxArrayLength);
|
|
69
81
|
}
|
|
70
82
|
return out;
|
|
71
83
|
}
|
|
@@ -111,19 +123,32 @@ function normalizeWhitelist(whitelist) {
|
|
|
111
123
|
function buildWhitelistHelpers(whitelist) {
|
|
112
124
|
const exact = new Set(whitelist);
|
|
113
125
|
const prefixes = whitelist.filter((w) => w.length > 0);
|
|
126
|
+
const pathCache = /* @__PURE__ */ new Map();
|
|
114
127
|
return {
|
|
115
128
|
exact,
|
|
116
129
|
prefixes,
|
|
117
130
|
isWhitelistedPath(pathParts) {
|
|
118
131
|
if (pathParts.length === 0) return false;
|
|
119
132
|
const full = pathParts.join(".");
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
const cached = pathCache.get(full);
|
|
134
|
+
if (cached !== void 0) return cached;
|
|
135
|
+
let result = false;
|
|
136
|
+
if (exact.has(full)) {
|
|
137
|
+
result = true;
|
|
138
|
+
} else if (exact.has(pathParts[pathParts.length - 1])) {
|
|
139
|
+
result = true;
|
|
140
|
+
} else {
|
|
141
|
+
for (const p of prefixes) {
|
|
142
|
+
if (full === p || full.startsWith(p + ".")) {
|
|
143
|
+
result = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (pathCache.size < 1e3) {
|
|
149
|
+
pathCache.set(full, result);
|
|
125
150
|
}
|
|
126
|
-
return
|
|
151
|
+
return result;
|
|
127
152
|
}
|
|
128
153
|
};
|
|
129
154
|
}
|
|
@@ -134,19 +159,23 @@ function setIn(target, path, value) {
|
|
|
134
159
|
let cur = target;
|
|
135
160
|
for (let i = 0; i < path.length - 1; i++) {
|
|
136
161
|
const k = path[i];
|
|
137
|
-
if (
|
|
162
|
+
if (DANGEROUS_KEYS.has(k)) return;
|
|
163
|
+
if (!isPlainObject(cur[k])) {
|
|
164
|
+
cur[k] = {};
|
|
165
|
+
}
|
|
138
166
|
cur = cur[k];
|
|
139
167
|
}
|
|
140
168
|
const lastKey = path[path.length - 1];
|
|
169
|
+
if (DANGEROUS_KEYS.has(lastKey)) return;
|
|
141
170
|
cur[lastKey] = value;
|
|
142
171
|
}
|
|
143
172
|
function moveWhitelistedFromPolluted(reqPart, polluted, isWhitelisted) {
|
|
144
|
-
function walk(node, path = []
|
|
173
|
+
function walk(node, path = []) {
|
|
145
174
|
for (const k of Object.keys(node)) {
|
|
146
175
|
const v = node[k];
|
|
147
176
|
const curPath = [...path, k];
|
|
148
177
|
if (isPlainObject(v)) {
|
|
149
|
-
walk(v, curPath
|
|
178
|
+
walk(v, curPath);
|
|
150
179
|
if (Object.keys(v).length === 0) {
|
|
151
180
|
delete node[k];
|
|
152
181
|
}
|
|
@@ -170,11 +199,13 @@ function detectAndReduce(input, opts) {
|
|
|
170
199
|
function processNode(node, path = [], depth = 0) {
|
|
171
200
|
if (node === null || node === void 0) return opts.preserveNull ? node : node;
|
|
172
201
|
if (Array.isArray(node)) {
|
|
173
|
-
const
|
|
202
|
+
const limit = opts.maxArrayLength ?? 1e3;
|
|
203
|
+
const limitedNode = node.slice(0, limit);
|
|
204
|
+
const mapped = limitedNode.map((v) => processNode(v, path, depth));
|
|
174
205
|
if (opts.mergeStrategy === "combine") {
|
|
175
206
|
return mergeValues(mapped, "combine");
|
|
176
207
|
}
|
|
177
|
-
setIn(polluted, path, safeDeepClone(
|
|
208
|
+
setIn(polluted, path, safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength));
|
|
178
209
|
pollutedKeys.push(path.join("."));
|
|
179
210
|
const reduced = mergeValues(mapped, opts.mergeStrategy);
|
|
180
211
|
return reduced;
|
|
@@ -188,7 +219,7 @@ function detectAndReduce(input, opts) {
|
|
|
188
219
|
if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {
|
|
189
220
|
throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);
|
|
190
221
|
}
|
|
191
|
-
const safeKey = sanitizeKey(rawKey);
|
|
222
|
+
const safeKey = sanitizeKey(rawKey, opts.maxKeyLength);
|
|
192
223
|
if (!safeKey) continue;
|
|
193
224
|
const child = node[rawKey];
|
|
194
225
|
const childPath = path.concat([safeKey]);
|
|
@@ -200,18 +231,20 @@ function detectAndReduce(input, opts) {
|
|
|
200
231
|
}
|
|
201
232
|
return node;
|
|
202
233
|
}
|
|
203
|
-
const cloned = safeDeepClone(input);
|
|
234
|
+
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);
|
|
204
235
|
const cleaned = processNode(cloned, [], 0);
|
|
205
236
|
return { cleaned, pollutedTree: polluted, pollutedKeys };
|
|
206
237
|
}
|
|
207
238
|
function sanitize(input, options = {}) {
|
|
208
|
-
const
|
|
239
|
+
const maxKeyLength = options.maxKeyLength ?? 200;
|
|
240
|
+
const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength) : input;
|
|
209
241
|
const whitelist = normalizeWhitelist(options.whitelist);
|
|
210
242
|
const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
|
|
211
243
|
const {
|
|
212
244
|
mergeStrategy = DEFAULT_STRATEGY,
|
|
213
245
|
maxDepth = 20,
|
|
214
246
|
maxKeys = 5e3,
|
|
247
|
+
maxArrayLength = 1e3,
|
|
215
248
|
trimValues = false,
|
|
216
249
|
preserveNull = true
|
|
217
250
|
} = options;
|
|
@@ -219,13 +252,49 @@ function sanitize(input, options = {}) {
|
|
|
219
252
|
mergeStrategy,
|
|
220
253
|
maxDepth,
|
|
221
254
|
maxKeys,
|
|
255
|
+
maxArrayLength,
|
|
256
|
+
maxKeyLength,
|
|
222
257
|
trimValues,
|
|
223
258
|
preserveNull
|
|
224
259
|
});
|
|
225
260
|
moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
|
|
226
261
|
return cleaned;
|
|
227
262
|
}
|
|
263
|
+
function validateOptions(options) {
|
|
264
|
+
if (options.maxDepth !== void 0 && (typeof options.maxDepth !== "number" || options.maxDepth < 1 || options.maxDepth > 100)) {
|
|
265
|
+
throw new TypeError("maxDepth must be a number between 1 and 100");
|
|
266
|
+
}
|
|
267
|
+
if (options.maxKeys !== void 0 && (typeof options.maxKeys !== "number" || options.maxKeys < 1)) {
|
|
268
|
+
throw new TypeError("maxKeys must be a positive number");
|
|
269
|
+
}
|
|
270
|
+
if (options.maxArrayLength !== void 0 && (typeof options.maxArrayLength !== "number" || options.maxArrayLength < 1)) {
|
|
271
|
+
throw new TypeError("maxArrayLength must be a positive number");
|
|
272
|
+
}
|
|
273
|
+
if (options.maxKeyLength !== void 0 && (typeof options.maxKeyLength !== "number" || options.maxKeyLength < 1 || options.maxKeyLength > 1e3)) {
|
|
274
|
+
throw new TypeError("maxKeyLength must be a number between 1 and 1000");
|
|
275
|
+
}
|
|
276
|
+
if (options.mergeStrategy !== void 0 && !["keepFirst", "keepLast", "combine"].includes(options.mergeStrategy)) {
|
|
277
|
+
throw new TypeError("mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'");
|
|
278
|
+
}
|
|
279
|
+
if (options.sources !== void 0 && !Array.isArray(options.sources)) {
|
|
280
|
+
throw new TypeError("sources must be an array");
|
|
281
|
+
}
|
|
282
|
+
if (options.sources !== void 0) {
|
|
283
|
+
for (const source of options.sources) {
|
|
284
|
+
if (!["query", "body", "params"].includes(source)) {
|
|
285
|
+
throw new TypeError("sources must only contain 'query', 'body', or 'params'");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (options.checkBodyContentType !== void 0 && !["urlencoded", "any", "none"].includes(options.checkBodyContentType)) {
|
|
290
|
+
throw new TypeError("checkBodyContentType must be 'urlencoded', 'any', or 'none'");
|
|
291
|
+
}
|
|
292
|
+
if (options.excludePaths !== void 0 && !Array.isArray(options.excludePaths)) {
|
|
293
|
+
throw new TypeError("excludePaths must be an array");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
228
296
|
function hppx(options = {}) {
|
|
297
|
+
validateOptions(options);
|
|
229
298
|
const {
|
|
230
299
|
whitelist = [],
|
|
231
300
|
mergeStrategy = DEFAULT_STRATEGY,
|
|
@@ -234,6 +303,8 @@ function hppx(options = {}) {
|
|
|
234
303
|
excludePaths = [],
|
|
235
304
|
maxDepth = 20,
|
|
236
305
|
maxKeys = 5e3,
|
|
306
|
+
maxArrayLength = 1e3,
|
|
307
|
+
maxKeyLength = 200,
|
|
237
308
|
trimValues = false,
|
|
238
309
|
preserveNull = true,
|
|
239
310
|
strict = false,
|
|
@@ -256,7 +327,7 @@ function hppx(options = {}) {
|
|
|
256
327
|
}
|
|
257
328
|
const part = req[source];
|
|
258
329
|
if (!isPlainObject(part)) continue;
|
|
259
|
-
const expandedPart = expandObjectPaths(part);
|
|
330
|
+
const expandedPart = expandObjectPaths(part, maxKeyLength);
|
|
260
331
|
const pollutedKey = `${source}Polluted`;
|
|
261
332
|
const processedKey = `__hppxProcessed_${source}`;
|
|
262
333
|
const hasProcessedBefore = Boolean(req[processedKey]);
|
|
@@ -265,28 +336,49 @@ function hppx(options = {}) {
|
|
|
265
336
|
mergeStrategy,
|
|
266
337
|
maxDepth,
|
|
267
338
|
maxKeys,
|
|
339
|
+
maxArrayLength,
|
|
340
|
+
maxKeyLength,
|
|
268
341
|
trimValues,
|
|
269
342
|
preserveNull
|
|
270
343
|
});
|
|
271
344
|
setReqPropertySafe(req, source, cleaned);
|
|
272
345
|
setReqPropertySafe(req, pollutedKey, pollutedTree);
|
|
273
346
|
req[processedKey] = true;
|
|
274
|
-
|
|
347
|
+
const sourceData = req[source];
|
|
348
|
+
const pollutedData = req[pollutedKey];
|
|
349
|
+
if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {
|
|
350
|
+
moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);
|
|
351
|
+
}
|
|
275
352
|
if (pollutedKeys.length > 0) {
|
|
276
353
|
anyPollutionDetected = true;
|
|
277
354
|
for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);
|
|
278
355
|
}
|
|
279
356
|
} else {
|
|
280
|
-
|
|
357
|
+
const sourceData = req[source];
|
|
358
|
+
const pollutedData = req[pollutedKey];
|
|
359
|
+
if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {
|
|
360
|
+
moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);
|
|
361
|
+
}
|
|
281
362
|
}
|
|
282
363
|
}
|
|
283
364
|
if (anyPollutionDetected) {
|
|
284
365
|
if (onPollutionDetected) {
|
|
285
366
|
try {
|
|
286
|
-
|
|
287
|
-
source
|
|
288
|
-
|
|
289
|
-
|
|
367
|
+
for (const source of sources) {
|
|
368
|
+
const pollutedKey = `${source}Polluted`;
|
|
369
|
+
const pollutedData = req[pollutedKey];
|
|
370
|
+
if (pollutedData && Object.keys(pollutedData).length > 0) {
|
|
371
|
+
const sourcePollutedKeys = allPollutedKeys.filter(
|
|
372
|
+
(k) => k.startsWith(`${source}.`)
|
|
373
|
+
);
|
|
374
|
+
if (sourcePollutedKeys.length > 0) {
|
|
375
|
+
onPollutionDetected(req, {
|
|
376
|
+
source,
|
|
377
|
+
pollutedKeys: sourcePollutedKeys
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
290
382
|
} catch (_) {
|
|
291
383
|
}
|
|
292
384
|
}
|
|
@@ -301,13 +393,18 @@ function hppx(options = {}) {
|
|
|
301
393
|
}
|
|
302
394
|
return next();
|
|
303
395
|
} catch (err) {
|
|
396
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
304
397
|
if (logger) {
|
|
305
398
|
try {
|
|
306
|
-
logger(
|
|
307
|
-
} catch (
|
|
399
|
+
logger(error);
|
|
400
|
+
} catch (logErr) {
|
|
401
|
+
if (process.env.NODE_ENV !== "production") {
|
|
402
|
+
console.error("[hppx] Logger failed:", logErr);
|
|
403
|
+
console.error("[hppx] Original error:", error);
|
|
404
|
+
}
|
|
308
405
|
}
|
|
309
406
|
}
|
|
310
|
-
return next(
|
|
407
|
+
return next(error);
|
|
311
408
|
}
|
|
312
409
|
};
|
|
313
410
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (req: any, info: { source: RequestSource; pollutedKeys: string[] }) => void;\r\n logger?: (err: unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n return key;\r\n}\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n return dotted.split(\".\").filter((s) => s.length > 0);\r\n}\r\n\r\nfunction expandObjectPaths(obj: Record<string, unknown>): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const value = (obj as any)[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: any, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T): T {\r\n if (Array.isArray(input)) {\r\n return input.map((v) => safeDeepClone(v)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k]);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n if (exact.has(full)) return true;\r\n // leaf match\r\n const leaf = pathParts[pathParts.length - 1]!;\r\n if (exact.has(leaf)) return true;\r\n // prefix match (treat any listed segment as prefix of a subtree)\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) return true;\r\n }\r\n return false;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: any = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n if (!isPlainObject(cur[k])) cur[k] = {};\r\n cur = cur[k];\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(\r\n node: Record<string, unknown>,\r\n path: string[] = [],\r\n parent?: Record<string, unknown>,\r\n ) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath, node);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete (node as any)[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete (node as any)[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<SanitizeOptions, \"mergeStrategy\" | \"maxDepth\" | \"maxKeys\" | \"trimValues\" | \"preserveNull\">\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n const mapped = node.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(node));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: any) => void;\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */ if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean((req as any)[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n (req as any)[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n onPollutionDetected(req, {\r\n source: \"query\",\r\n pollutedKeys: allPollutedKeys,\r\n });\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n if (logger) {\r\n try {\r\n logger(err);\r\n } catch (_) {\r\n /* noop */\r\n }\r\n }\r\n return next(err);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n maxArrayLength?: number;\r\n maxKeyLength?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (\r\n req: Record<string, unknown>,\r\n info: { source: RequestSource; pollutedKeys: string[] },\r\n ) => void;\r\n logger?: (err: Error | unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string, maxKeyLength?: number): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n // Prevent excessively long keys that could cause DoS\r\n const maxLen = maxKeyLength ?? 200;\r\n if (key.length > maxLen) return null;\r\n // Prevent keys that are only dots or brackets (malformed) - but allow single dot as it's valid\r\n if (key.length > 1 && /^[.\\[\\]]+$/.test(key)) return null;\r\n return key;\r\n}\r\n\r\n// Cache for parsed path segments to improve performance\r\nconst pathSegmentCache = new Map<string, string[]>();\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Check cache first\r\n const cached = pathSegmentCache.get(key);\r\n if (cached) return cached;\r\n\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n const result = dotted.split(\".\").filter((s) => s.length > 0);\r\n\r\n // Cache the result (limit cache size)\r\n if (pathSegmentCache.size < 500) {\r\n pathSegmentCache.set(key, result);\r\n }\r\n\r\n return result;\r\n}\r\n\r\nfunction expandObjectPaths(\r\n obj: Record<string, unknown>,\r\n maxKeyLength?: number,\r\n): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey, maxKeyLength);\r\n if (!safeKey) continue;\r\n const value = obj[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>, maxKeyLength)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: Record<string, unknown>, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T, maxKeyLength?: number, maxArrayLength?: number): T {\r\n if (Array.isArray(input)) {\r\n // Limit array length to prevent memory exhaustion\r\n const limit = maxArrayLength ?? 1000;\r\n const limited = input.slice(0, limit);\r\n return limited.map((v) => safeDeepClone(v, maxKeyLength, maxArrayLength)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k, maxKeyLength)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k], maxKeyLength, maxArrayLength);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n // Pre-build a cache for commonly checked paths for performance\r\n const pathCache = new Map<string, boolean>();\r\n\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n\r\n // Check cache first for performance\r\n const cached = pathCache.get(full);\r\n if (cached !== undefined) return cached;\r\n\r\n let result = false;\r\n\r\n // Exact match\r\n if (exact.has(full)) {\r\n result = true;\r\n }\r\n // Leaf match\r\n else if (exact.has(pathParts[pathParts.length - 1]!)) {\r\n result = true;\r\n }\r\n // Prefix match (treat any listed segment as prefix of a subtree)\r\n else {\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) {\r\n result = true;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // Cache the result (limit cache size to prevent memory issues)\r\n if (pathCache.size < 1000) {\r\n pathCache.set(full, result);\r\n }\r\n\r\n return result;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: Record<string, unknown> = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n // Additional prototype pollution protection\r\n if (DANGEROUS_KEYS.has(k)) return;\r\n if (!isPlainObject(cur[k])) {\r\n // Create a new plain object to avoid pollution\r\n cur[k] = {};\r\n }\r\n cur = cur[k] as Record<string, unknown>;\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n // Final check on the last key\r\n if (DANGEROUS_KEYS.has(lastKey)) return;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(node: Record<string, unknown>, path: string[] = []) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete node[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete node[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<\r\n SanitizeOptions,\r\n | \"mergeStrategy\"\r\n | \"maxDepth\"\r\n | \"maxKeys\"\r\n | \"maxArrayLength\"\r\n | \"maxKeyLength\"\r\n | \"trimValues\"\r\n | \"preserveNull\"\r\n >\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n // Limit array length to prevent DoS\r\n const limit = opts.maxArrayLength ?? 1000;\r\n const limitedNode = node.slice(0, limit);\r\n\r\n const mapped = limitedNode.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey, opts.maxKeyLength);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const maxKeyLength = options.maxKeyLength ?? 200;\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n maxArrayLength = 1000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n maxArrayLength,\r\n maxKeyLength,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: unknown) => void;\r\n\r\nfunction validateOptions(options: HppxOptions): void {\r\n if (\r\n options.maxDepth !== undefined &&\r\n (typeof options.maxDepth !== \"number\" || options.maxDepth < 1 || options.maxDepth > 100)\r\n ) {\r\n throw new TypeError(\"maxDepth must be a number between 1 and 100\");\r\n }\r\n if (\r\n options.maxKeys !== undefined &&\r\n (typeof options.maxKeys !== \"number\" || options.maxKeys < 1)\r\n ) {\r\n throw new TypeError(\"maxKeys must be a positive number\");\r\n }\r\n if (\r\n options.maxArrayLength !== undefined &&\r\n (typeof options.maxArrayLength !== \"number\" || options.maxArrayLength < 1)\r\n ) {\r\n throw new TypeError(\"maxArrayLength must be a positive number\");\r\n }\r\n if (\r\n options.maxKeyLength !== undefined &&\r\n (typeof options.maxKeyLength !== \"number\" ||\r\n options.maxKeyLength < 1 ||\r\n options.maxKeyLength > 1000)\r\n ) {\r\n throw new TypeError(\"maxKeyLength must be a number between 1 and 1000\");\r\n }\r\n if (\r\n options.mergeStrategy !== undefined &&\r\n ![\"keepFirst\", \"keepLast\", \"combine\"].includes(options.mergeStrategy)\r\n ) {\r\n throw new TypeError(\"mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'\");\r\n }\r\n if (options.sources !== undefined && !Array.isArray(options.sources)) {\r\n throw new TypeError(\"sources must be an array\");\r\n }\r\n if (options.sources !== undefined) {\r\n for (const source of options.sources) {\r\n if (![\"query\", \"body\", \"params\"].includes(source)) {\r\n throw new TypeError(\"sources must only contain 'query', 'body', or 'params'\");\r\n }\r\n }\r\n }\r\n if (\r\n options.checkBodyContentType !== undefined &&\r\n ![\"urlencoded\", \"any\", \"none\"].includes(options.checkBodyContentType)\r\n ) {\r\n throw new TypeError(\"checkBodyContentType must be 'urlencoded', 'any', or 'none'\");\r\n }\r\n if (options.excludePaths !== undefined && !Array.isArray(options.excludePaths)) {\r\n throw new TypeError(\"excludePaths must be an array\");\r\n }\r\n}\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n // Validate options on middleware creation\r\n validateOptions(options);\r\n\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n maxArrayLength = 1000,\r\n maxKeyLength = 200,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */\r\n if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part, maxKeyLength);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean(req[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n maxArrayLength,\r\n maxKeyLength,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n req[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n const sourceData = req[source];\r\n const pollutedData = req[pollutedKey];\r\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\r\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\r\n }\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n const sourceData = req[source];\r\n const pollutedData = req[pollutedKey];\r\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\r\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\r\n }\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n // Determine which sources had pollution\r\n for (const source of sources) {\r\n const pollutedKey = `${source}Polluted`;\r\n const pollutedData = req[pollutedKey];\r\n if (pollutedData && Object.keys(pollutedData).length > 0) {\r\n const sourcePollutedKeys = allPollutedKeys.filter((k) =>\r\n k.startsWith(`${source}.`),\r\n );\r\n if (sourcePollutedKeys.length > 0) {\r\n onPollutionDetected(req, {\r\n source: source,\r\n pollutedKeys: sourcePollutedKeys,\r\n });\r\n }\r\n }\r\n }\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n // Enhanced error handling with detailed logging\r\n const error = err instanceof Error ? err : new Error(String(err));\r\n\r\n if (logger) {\r\n try {\r\n logger(error);\r\n } catch (logErr) {\r\n // If custom logger fails, use console.error as fallback in development\r\n if (process.env.NODE_ENV !== \"production\") {\r\n console.error(\"[hppx] Logger failed:\", logErr);\r\n console.error(\"[hppx] Original error:\", error);\r\n }\r\n }\r\n }\r\n\r\n // Pass error to next middleware for proper error handling\r\n return next(error);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hppx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Superior HTTP Parameter Pollution protection middleware with modern TypeScript, robust sanitizer, and extensive tests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hiprax",
|
|
@@ -47,6 +47,18 @@
|
|
|
47
47
|
"lint": "eslint \"{src,tests}/**/*.{ts,tsx}\"",
|
|
48
48
|
"format": "prettier --write \"**/*.{ts,tsx,js,json,md,yml,yaml}\""
|
|
49
49
|
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"hpp",
|
|
52
|
+
"http",
|
|
53
|
+
"parameter",
|
|
54
|
+
"pollution",
|
|
55
|
+
"protection",
|
|
56
|
+
"middleware",
|
|
57
|
+
"typescript",
|
|
58
|
+
"nodejs",
|
|
59
|
+
"express",
|
|
60
|
+
"security"
|
|
61
|
+
],
|
|
50
62
|
"devDependencies": {
|
|
51
63
|
"@types/express": "^5.0.5",
|
|
52
64
|
"@types/jest": "^30.0.0",
|