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 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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-blue.svg)](https://www.typescriptlang.org/)
7
- [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9.3-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/)
8
8
 
9
9
  ## Features
10
10
 
11
- - Array merging strategies: `keepFirst`, `keepLast` (default), `combine`
12
- - Safe-by-default: blocks `__proto__`, `prototype`, `constructor`
13
- - Nested whitelist with dot-notation and leaf matching
14
- - Records polluted parameters on the request (`queryPolluted`, `bodyPolluted`, `paramsPolluted`)
15
- - Works with multiple middlewares on different routes (whitelists applied incrementally)
16
- - DoS-guards: `maxDepth`, `maxKeys`
17
- - Fully typed API and helpers (`sanitize`)
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 options:
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
- - `sources?: Array<'query'|'body'|'params'>` — which request parts to sanitize
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
- - `maxDepth?: number` and `maxKeys?: number` — DoS protections
67
- - `strict?: boolean` — if pollution detected, immediately respond 400
68
- - `onPollutionDetected?: (req, info) => void` — callback on detection
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: 'any' }));
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: ['/public', '/assets*'] }));
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 'hppx';
120
+ import { sanitize } from "hppx";
99
121
 
100
122
  const clean = sanitize(payload, {
101
- whitelist: ['user.tags'],
102
- mergeStrategy: 'keepFirst',
123
+ whitelist: ["user.tags"],
124
+ mergeStrategy: "keepFirst",
103
125
  });
104
126
  ```
105
127
 
106
- ## Notes
128
+ ## Security Best Practices
129
+
130
+ ### Input Validation
131
+
132
+ Always combine HPP protection with additional input validation:
107
133
 
108
- - Arrays are reduced by default; whitelisted paths are preserved as arrays.
109
- - Dangerous keys like `__proto__`, `prototype`, `constructor` are removed.
110
- - DoS protections are available via `maxDepth` and `maxKeys`.
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/@hiprax/hppx)
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
- return dotted.split(".").filter((s) => s.length > 0);
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
- return input.map((v) => safeDeepClone(v));
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
- if (exact.has(full)) return true;
149
- const leaf = pathParts[pathParts.length - 1];
150
- if (exact.has(leaf)) return true;
151
- for (const p of prefixes) {
152
- if (full === p || full.startsWith(p + ".")) return true;
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 false;
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 (!isPlainObject(cur[k])) cur[k] = {};
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 = [], parent) {
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, node);
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 mapped = node.map((v) => processNode(v, path, depth));
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(node));
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 expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;
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
- moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
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
- moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
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
- onPollutionDetected(req, {
315
- source: "query",
316
- pollutedKeys: allPollutedKeys
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(err);
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(err);
435
+ return next(error);
339
436
  }
340
437
  };
341
438
  }
@@ -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: any, info: {
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?: any) => void;
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: any, info: {
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?: any) => void;
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
- return dotted.split(".").filter((s) => s.length > 0);
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
- return input.map((v) => safeDeepClone(v));
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
- if (exact.has(full)) return true;
121
- const leaf = pathParts[pathParts.length - 1];
122
- if (exact.has(leaf)) return true;
123
- for (const p of prefixes) {
124
- if (full === p || full.startsWith(p + ".")) return true;
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 false;
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 (!isPlainObject(cur[k])) cur[k] = {};
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 = [], parent) {
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, node);
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 mapped = node.map((v) => processNode(v, path, depth));
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(node));
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 expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;
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
- moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
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
- moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
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
- onPollutionDetected(req, {
287
- source: "query",
288
- pollutedKeys: allPollutedKeys
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(err);
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(err);
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.0",
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",