hppx 0.1.7 → 0.1.10
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/LICENSE +0 -0
- package/README.md +189 -91
- package/dist/index.cjs +61 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +61 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/index.d.cts +0 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
# hppx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Superior HTTP Parameter Pollution protection middleware** for Node.js/Express, written in TypeScript. It sanitizes `req.query`, `req.body`, and `req.params`, blocks prototype-pollution keys, supports nested whitelists, multiple merge strategies, and plays nicely with stacked middlewares.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://www.npmjs.com/package/hppx)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](#)
|
|
10
|
+
|
|
11
|
+
---
|
|
8
12
|
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
15
|
+
- **Zero runtime dependencies** — minimal attack surface and bundle size
|
|
16
|
+
- **Multiple merge strategies** — `keepFirst`, `keepLast` (default), `combine`
|
|
17
|
+
- **Enhanced security:**
|
|
13
18
|
- Blocks dangerous keys: `__proto__`, `prototype`, `constructor`
|
|
14
19
|
- Prevents null-byte injection in keys
|
|
20
|
+
- Rejects malformed keys (dot/bracket-only patterns)
|
|
15
21
|
- Validates key lengths to prevent DoS attacks
|
|
16
22
|
- Limits array sizes to prevent memory exhaustion
|
|
17
|
-
- **Flexible whitelisting
|
|
18
|
-
- **Pollution tracking
|
|
19
|
-
- **Multi-middleware support
|
|
20
|
-
- **DoS protection
|
|
21
|
-
- **Performance optimized
|
|
22
|
-
- **Fully typed API
|
|
23
|
+
- **Flexible whitelisting** — nested whitelist with dot-notation and leaf matching
|
|
24
|
+
- **Pollution tracking** — records polluted parameters on the request (`queryPolluted`, `bodyPolluted`, `paramsPolluted`)
|
|
25
|
+
- **Multi-middleware support** — works with multiple middlewares on different routes (whitelists applied incrementally)
|
|
26
|
+
- **DoS protection** — `maxDepth`, `maxKeys`, `maxArrayLength`, `maxKeyLength`
|
|
27
|
+
- **Performance optimized** — path caching and Set-based lookups for fast whitelist checks
|
|
28
|
+
- **Fully typed API** — TypeScript-first with comprehensive type definitions for both ESM and CommonJS
|
|
29
|
+
|
|
30
|
+
---
|
|
23
31
|
|
|
24
|
-
##
|
|
32
|
+
## Installation
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
35
|
npm install hppx
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
31
41
|
|
|
32
42
|
### ESM (ES Modules)
|
|
33
43
|
|
|
@@ -85,77 +95,153 @@ app.get("/search", (req, res) => {
|
|
|
85
95
|
});
|
|
86
96
|
```
|
|
87
97
|
|
|
98
|
+
---
|
|
99
|
+
|
|
88
100
|
## API
|
|
89
101
|
|
|
90
|
-
###
|
|
102
|
+
### Default Export: `hppx(options?: HppxOptions)`
|
|
103
|
+
|
|
104
|
+
Creates an Express-compatible middleware. Applies sanitization to each selected source and exposes `*.Polluted` objects on the request.
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
> **Note:** Invalid options throw a `TypeError` at middleware creation time, not at request time. This ensures misconfiguration is caught early.
|
|
93
107
|
|
|
94
|
-
####
|
|
108
|
+
#### Options
|
|
95
109
|
|
|
96
110
|
**Whitelist & Strategy:**
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
| --------------- | ---------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `whitelist` | `string[] \| string` | `[]` | Keys allowed to remain as arrays. Supports dot-notation (`"user.tags"`) and leaf matching (`"tags"` matches any path ending in `tags`). |
|
|
115
|
+
| `mergeStrategy` | `'keepFirst' \| 'keepLast' \| 'combine'` | `'keepLast'` | How to reduce duplicate/array parameters when not whitelisted. `keepFirst` takes the first value, `keepLast` takes the last, `combine` flattens all values into a single array. |
|
|
100
116
|
|
|
101
117
|
**Source Selection:**
|
|
102
118
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
| Option | Type | Default | Description |
|
|
120
|
+
| ---------------------- | -------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
+
| `sources` | `Array<'query' \| 'body' \| 'params'>` | `['query', 'body', 'params']` | Which request parts to sanitize. |
|
|
122
|
+
| `checkBodyContentType` | `'urlencoded' \| 'any' \| 'none'` | `'urlencoded'` | When to process `req.body`. `urlencoded` only processes URL-encoded bodies, `any` processes all content types, `none` skips body processing entirely. |
|
|
123
|
+
| `excludePaths` | `string[]` | `[]` | Paths to exclude from sanitization. Supports `*` wildcard suffix (e.g., `"/assets*"`). |
|
|
106
124
|
|
|
107
125
|
**Security Limits (DoS Protection):**
|
|
108
126
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
| Option | Type | Default | Range | Description |
|
|
128
|
+
| ---------------- | -------- | ------- | -------- | ------------------------------------------------------------------------------------- |
|
|
129
|
+
| `maxDepth` | `number` | `20` | 1 - 100 | Maximum object nesting depth. Exceeding this throws an error passed to `next()`. |
|
|
130
|
+
| `maxKeys` | `number` | `5000` | >= 1 | Maximum number of keys to process. Exceeding this throws an error passed to `next()`. |
|
|
131
|
+
| `maxArrayLength` | `number` | `1000` | >= 1 | Maximum array length. Arrays are truncated before processing. |
|
|
132
|
+
| `maxKeyLength` | `number` | `200` | 1 - 1000 | Maximum key string length. Longer keys are silently dropped. |
|
|
133
|
+
|
|
134
|
+
**Behavior & Callbacks:**
|
|
135
|
+
|
|
136
|
+
| Option | Type | Default | Description |
|
|
137
|
+
| --------------------- | --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `trimValues` | `boolean` | `false` | Trim whitespace from string values. |
|
|
139
|
+
| `preserveNull` | `boolean` | `true` | Preserve `null` values in the output. |
|
|
140
|
+
| `strict` | `boolean` | `false` | Immediately respond with HTTP 400 when pollution is detected. Response includes `error`, `message`, `pollutedParameters`, and `code` (`"HPP_DETECTED"`) fields. |
|
|
141
|
+
| `onPollutionDetected` | `(req, info) => void` | — | Callback fired on pollution detection. Called **once per polluted source** (e.g., fires twice if both query and body are polluted). `info` contains `{ source: RequestSource, pollutedKeys: string[] }`. |
|
|
142
|
+
| `logger` | `(err: Error \| unknown) => void` | — | Custom logger for errors and pollution warnings. Receives `string` for pollution warnings and `Error` for caught errors. Falls back to `console.warn`/`console.error` if the logger throws. |
|
|
143
|
+
| `logPollution` | `boolean` | `true` | Enable automatic logging when pollution is detected. |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### Named Export: `sanitize(input, options?)`
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Sanitize a plain object using the same rules as the middleware. Returns only the cleaned object (polluted data is not returned — use the middleware if you need `req.queryPolluted` etc.).
|
|
154
|
+
|
|
155
|
+
**ESM:**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { sanitize } from "hppx";
|
|
159
|
+
|
|
160
|
+
const clean = sanitize(payload, {
|
|
161
|
+
whitelist: ["user.tags"],
|
|
162
|
+
mergeStrategy: "keepFirst",
|
|
163
|
+
});
|
|
164
|
+
```
|
|
113
165
|
|
|
114
|
-
**
|
|
166
|
+
**CommonJS:**
|
|
115
167
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- `strict?: boolean` — if pollution detected, immediately respond with 400 error
|
|
119
|
-
- `onPollutionDetected?: (req, info) => void` — callback on pollution detection
|
|
120
|
-
- `logger?: (err: Error | string) => void` — custom logger for errors and pollution warnings
|
|
121
|
-
- `logPollution?: boolean` — enable automatic logging when pollution is detected (default: true)
|
|
168
|
+
```javascript
|
|
169
|
+
const { sanitize } = require("hppx");
|
|
122
170
|
|
|
123
|
-
|
|
171
|
+
const clean = sanitize(payload, {
|
|
172
|
+
whitelist: ["user.tags"],
|
|
173
|
+
mergeStrategy: "keepFirst",
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
124
178
|
|
|
125
|
-
|
|
179
|
+
### Exported Types
|
|
126
180
|
|
|
127
|
-
|
|
181
|
+
All types are available for both ESM and CommonJS consumers:
|
|
128
182
|
|
|
129
|
-
|
|
183
|
+
```typescript
|
|
184
|
+
import type {
|
|
185
|
+
RequestSource, // "query" | "body" | "params"
|
|
186
|
+
MergeStrategy, // "keepFirst" | "keepLast" | "combine"
|
|
187
|
+
SanitizeOptions, // Options for sanitize()
|
|
188
|
+
HppxOptions, // Full middleware options (extends SanitizeOptions)
|
|
189
|
+
SanitizedResult, // { cleaned, pollutedTree, pollutedKeys }
|
|
190
|
+
} from "hppx";
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Exported Constants
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY } from "hppx";
|
|
197
|
+
|
|
198
|
+
DANGEROUS_KEYS; // Set<string> — {"__proto__", "prototype", "constructor"}
|
|
199
|
+
DEFAULT_SOURCES; // ["query", "body", "params"]
|
|
200
|
+
DEFAULT_STRATEGY; // "keepLast"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Advanced Usage
|
|
206
|
+
|
|
207
|
+
### Strict Mode (Respond 400 on Pollution)
|
|
130
208
|
|
|
131
209
|
```typescript
|
|
132
210
|
app.use(hppx({ strict: true }));
|
|
211
|
+
|
|
212
|
+
// Polluted requests receive:
|
|
213
|
+
// {
|
|
214
|
+
// "error": "Bad Request",
|
|
215
|
+
// "message": "HTTP Parameter Pollution detected",
|
|
216
|
+
// "pollutedParameters": ["query.x"],
|
|
217
|
+
// "code": "HPP_DETECTED"
|
|
218
|
+
// }
|
|
133
219
|
```
|
|
134
220
|
|
|
135
|
-
### Process JSON
|
|
221
|
+
### Process JSON Bodies Too
|
|
136
222
|
|
|
137
223
|
```typescript
|
|
138
224
|
app.use(express.json());
|
|
139
225
|
app.use(hppx({ checkBodyContentType: "any" }));
|
|
140
226
|
```
|
|
141
227
|
|
|
142
|
-
### Exclude
|
|
228
|
+
### Exclude Specific Paths
|
|
143
229
|
|
|
144
230
|
```typescript
|
|
145
231
|
app.use(hppx({ excludePaths: ["/public", "/assets*"] }));
|
|
146
232
|
```
|
|
147
233
|
|
|
148
|
-
### Custom
|
|
234
|
+
### Custom Logging
|
|
149
235
|
|
|
150
236
|
```typescript
|
|
151
237
|
// Use your application's logger
|
|
152
238
|
app.use(
|
|
153
239
|
hppx({
|
|
154
|
-
logger: (
|
|
155
|
-
if (typeof
|
|
156
|
-
myLogger.warn(
|
|
240
|
+
logger: (msg) => {
|
|
241
|
+
if (typeof msg === "string") {
|
|
242
|
+
myLogger.warn(msg); // Pollution warnings
|
|
157
243
|
} else {
|
|
158
|
-
myLogger.error(
|
|
244
|
+
myLogger.error(msg); // Errors
|
|
159
245
|
}
|
|
160
246
|
},
|
|
161
247
|
}),
|
|
@@ -165,55 +251,74 @@ app.use(
|
|
|
165
251
|
app.use(hppx({ logPollution: false }));
|
|
166
252
|
```
|
|
167
253
|
|
|
168
|
-
###
|
|
254
|
+
### Multi-Middleware Stacking
|
|
255
|
+
|
|
256
|
+
hppx supports incremental whitelisting across multiple middleware instances. Each subsequent middleware applies its own whitelist to the already-collected polluted data:
|
|
169
257
|
|
|
170
258
|
```typescript
|
|
171
|
-
|
|
259
|
+
// Global middleware — whitelist "a"
|
|
260
|
+
app.use(hppx({ whitelist: ["a"] }));
|
|
172
261
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
262
|
+
// Route-level middleware — additionally whitelist "b" and "c"
|
|
263
|
+
const router = express.Router();
|
|
264
|
+
router.use(hppx({ whitelist: ["b", "c"] }));
|
|
265
|
+
|
|
266
|
+
// On this route, "a", "b", and "c" are all allowed as arrays
|
|
267
|
+
router.get("/data", (req, res) => {
|
|
268
|
+
res.json({ query: req.query });
|
|
176
269
|
});
|
|
177
|
-
```
|
|
178
270
|
|
|
179
|
-
|
|
271
|
+
app.use("/api", router);
|
|
272
|
+
```
|
|
180
273
|
|
|
181
|
-
|
|
182
|
-
const { sanitize } = require("hppx");
|
|
274
|
+
### Pollution Detection Callback
|
|
183
275
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
276
|
+
```typescript
|
|
277
|
+
app.use(
|
|
278
|
+
hppx({
|
|
279
|
+
onPollutionDetected: (req, info) => {
|
|
280
|
+
// Called once per polluted source (query, body, params)
|
|
281
|
+
securityLogger.warn("HPP detected", {
|
|
282
|
+
source: info.source,
|
|
283
|
+
pollutedKeys: info.pollutedKeys,
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
188
288
|
```
|
|
189
289
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
### Input Validation
|
|
290
|
+
---
|
|
193
291
|
|
|
194
|
-
|
|
292
|
+
## Security
|
|
195
293
|
|
|
196
|
-
|
|
197
|
-
- Validate data types and ranges after sanitization
|
|
198
|
-
- Never trust user input, even after sanitization
|
|
294
|
+
### What hppx Protects Against
|
|
199
295
|
|
|
200
|
-
|
|
296
|
+
| Threat | Protection |
|
|
297
|
+
| ------------------------ | ---------------------------------------------------------------------------------- |
|
|
298
|
+
| **Parameter pollution** | Duplicate parameters are reduced to a single value via the chosen merge strategy |
|
|
299
|
+
| **Prototype pollution** | `__proto__`, `constructor`, `prototype` keys are blocked at every processing level |
|
|
300
|
+
| **DoS via deep nesting** | `maxDepth` limit throws error on excessive nesting |
|
|
301
|
+
| **DoS via key flooding** | `maxKeys` limit throws error when key count is exceeded |
|
|
302
|
+
| **DoS via large arrays** | `maxArrayLength` truncates arrays before processing |
|
|
303
|
+
| **DoS via long keys** | `maxKeyLength` silently drops excessively long keys |
|
|
304
|
+
| **Null-byte injection** | Keys containing `\u0000` are silently dropped |
|
|
305
|
+
| **Malformed keys** | Keys consisting only of dots/brackets (e.g., `"..."`, `"[["`) are dropped |
|
|
201
306
|
|
|
202
|
-
|
|
307
|
+
### Production Configuration
|
|
203
308
|
|
|
204
|
-
```
|
|
309
|
+
```typescript
|
|
205
310
|
app.use(
|
|
206
311
|
hppx({
|
|
207
|
-
maxDepth: 10,
|
|
208
|
-
maxKeys: 1000,
|
|
209
|
-
maxArrayLength: 100,
|
|
210
|
-
maxKeyLength: 100,
|
|
211
|
-
strict: true,
|
|
312
|
+
maxDepth: 10,
|
|
313
|
+
maxKeys: 1000,
|
|
314
|
+
maxArrayLength: 100,
|
|
315
|
+
maxKeyLength: 100,
|
|
316
|
+
strict: true,
|
|
212
317
|
onPollutionDetected: (req, info) => {
|
|
213
|
-
// Log security events for monitoring
|
|
214
318
|
securityLogger.warn("HPP detected", {
|
|
215
319
|
ip: req.ip,
|
|
216
320
|
path: req.path,
|
|
321
|
+
source: info.source,
|
|
217
322
|
pollutedKeys: info.pollutedKeys,
|
|
218
323
|
});
|
|
219
324
|
},
|
|
@@ -221,33 +326,26 @@ app.use(
|
|
|
221
326
|
);
|
|
222
327
|
```
|
|
223
328
|
|
|
224
|
-
### What
|
|
329
|
+
### What hppx Does NOT Protect Against
|
|
225
330
|
|
|
226
|
-
|
|
227
|
-
- **Prototype pollution**: Attacks via `__proto__`, `constructor`, `prototype`
|
|
228
|
-
- **DoS attacks**: Excessive nesting, too many keys, huge arrays
|
|
229
|
-
- **Null-byte injection**: Keys containing null characters (`\u0000`)
|
|
331
|
+
hppx is not a complete security solution. You still need:
|
|
230
332
|
|
|
231
|
-
|
|
333
|
+
- **SQL injection protection** — use parameterized queries
|
|
334
|
+
- **XSS protection** — sanitize output, use CSP headers
|
|
335
|
+
- **CSRF protection** — use CSRF tokens
|
|
336
|
+
- **Authentication/Authorization** — validate user permissions
|
|
337
|
+
- **Rate limiting** — prevent brute-force attacks
|
|
338
|
+
- **Input validation** — use schema validation libraries (Joi, Yup, Zod) alongside hppx
|
|
232
339
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
- **SQL injection protection**: Use parameterized queries
|
|
236
|
-
- **XSS protection**: Sanitize output, use CSP headers
|
|
237
|
-
- **CSRF protection**: Use CSRF tokens
|
|
238
|
-
- **Authentication/Authorization**: Validate user permissions
|
|
239
|
-
- **Rate limiting**: Prevent brute-force attacks
|
|
340
|
+
---
|
|
240
341
|
|
|
241
|
-
##
|
|
342
|
+
## License
|
|
242
343
|
|
|
243
344
|
MIT License - see [LICENSE](LICENSE) file for details.
|
|
244
345
|
|
|
245
|
-
##
|
|
346
|
+
## Links
|
|
246
347
|
|
|
247
348
|
- [NPM Package](https://www.npmjs.com/package/hppx)
|
|
248
349
|
- [GitHub Repository](https://github.com/Hiprax/hppx)
|
|
249
350
|
- [Issue Tracker](https://github.com/Hiprax/hppx/issues)
|
|
250
|
-
|
|
251
|
-
---
|
|
252
|
-
|
|
253
|
-
### **Made with ❤️ for secure applications**
|
|
351
|
+
- [Changelog](CHANGELOG.md)
|
package/dist/index.cjs
CHANGED
|
@@ -55,13 +55,25 @@ function parsePathSegments(key) {
|
|
|
55
55
|
}
|
|
56
56
|
return result;
|
|
57
57
|
}
|
|
58
|
-
function expandObjectPaths(obj, maxKeyLength) {
|
|
58
|
+
function expandObjectPaths(obj, maxKeyLength, maxDepth = 20, currentDepth = 0, seen) {
|
|
59
|
+
if (currentDepth > maxDepth) {
|
|
60
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
61
|
+
}
|
|
62
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
63
|
+
if (seenSet.has(obj)) return {};
|
|
64
|
+
seenSet.add(obj);
|
|
59
65
|
const result = {};
|
|
60
66
|
for (const rawKey of Object.keys(obj)) {
|
|
61
67
|
const safeKey = sanitizeKey(rawKey, maxKeyLength);
|
|
62
68
|
if (!safeKey) continue;
|
|
63
69
|
const value = obj[rawKey];
|
|
64
|
-
const expandedValue = isPlainObject(value) ? expandObjectPaths(
|
|
70
|
+
const expandedValue = isPlainObject(value) ? expandObjectPaths(
|
|
71
|
+
value,
|
|
72
|
+
maxKeyLength,
|
|
73
|
+
maxDepth,
|
|
74
|
+
currentDepth + 1,
|
|
75
|
+
seenSet
|
|
76
|
+
) : value;
|
|
65
77
|
if (safeKey.includes(".") || safeKey.includes("[")) {
|
|
66
78
|
const segments = parsePathSegments(safeKey);
|
|
67
79
|
if (segments.length > 0) {
|
|
@@ -95,17 +107,38 @@ function setReqPropertySafe(target, key, value) {
|
|
|
95
107
|
} catch (_) {
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
|
-
function safeDeepClone(input, maxKeyLength, maxArrayLength) {
|
|
110
|
+
function safeDeepClone(input, maxKeyLength, maxArrayLength, maxDepth = 20, currentDepth = 0, seen) {
|
|
99
111
|
if (Array.isArray(input)) {
|
|
112
|
+
if (currentDepth > maxDepth) {
|
|
113
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
114
|
+
}
|
|
115
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
116
|
+
if (seenSet.has(input)) return [];
|
|
117
|
+
seenSet.add(input);
|
|
100
118
|
const limit = maxArrayLength ?? 1e3;
|
|
101
119
|
const limited = input.slice(0, limit);
|
|
102
|
-
return limited.map(
|
|
120
|
+
return limited.map(
|
|
121
|
+
(v) => safeDeepClone(v, maxKeyLength, maxArrayLength, maxDepth, currentDepth + 1, seenSet)
|
|
122
|
+
);
|
|
103
123
|
}
|
|
104
124
|
if (isPlainObject(input)) {
|
|
125
|
+
if (currentDepth > maxDepth) {
|
|
126
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
127
|
+
}
|
|
128
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
129
|
+
if (seenSet.has(input)) return {};
|
|
130
|
+
seenSet.add(input);
|
|
105
131
|
const out = {};
|
|
106
132
|
for (const k of Object.keys(input)) {
|
|
107
133
|
if (!sanitizeKey(k, maxKeyLength)) continue;
|
|
108
|
-
out[k] = safeDeepClone(
|
|
134
|
+
out[k] = safeDeepClone(
|
|
135
|
+
input[k],
|
|
136
|
+
maxKeyLength,
|
|
137
|
+
maxArrayLength,
|
|
138
|
+
maxDepth,
|
|
139
|
+
currentDepth + 1,
|
|
140
|
+
seenSet
|
|
141
|
+
);
|
|
109
142
|
}
|
|
110
143
|
return out;
|
|
111
144
|
}
|
|
@@ -123,6 +156,7 @@ function mergeValues(values, strategy) {
|
|
|
123
156
|
else acc.push(v);
|
|
124
157
|
return acc;
|
|
125
158
|
}, []);
|
|
159
|
+
/* istanbul ignore next -- unreachable: strategy is validated before reaching mergeValues */
|
|
126
160
|
default:
|
|
127
161
|
return values[values.length - 1];
|
|
128
162
|
}
|
|
@@ -225,7 +259,8 @@ function detectAndReduce(input, opts) {
|
|
|
225
259
|
const polluted = {};
|
|
226
260
|
const pollutedKeys = [];
|
|
227
261
|
function processNode(node, path = [], depth = 0) {
|
|
228
|
-
if (node === null
|
|
262
|
+
if (node === null) return opts.preserveNull ? null : void 0;
|
|
263
|
+
if (node === void 0) return node;
|
|
229
264
|
if (Array.isArray(node)) {
|
|
230
265
|
const limit = opts.maxArrayLength ?? 1e3;
|
|
231
266
|
const limitedNode = node.slice(0, limit);
|
|
@@ -233,7 +268,11 @@ function detectAndReduce(input, opts) {
|
|
|
233
268
|
if (opts.mergeStrategy === "combine") {
|
|
234
269
|
return mergeValues(mapped, "combine");
|
|
235
270
|
}
|
|
236
|
-
setIn(
|
|
271
|
+
setIn(
|
|
272
|
+
polluted,
|
|
273
|
+
path,
|
|
274
|
+
safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth)
|
|
275
|
+
);
|
|
237
276
|
pollutedKeys.push(path.join("."));
|
|
238
277
|
const reduced = mergeValues(mapped, opts.mergeStrategy);
|
|
239
278
|
return reduced;
|
|
@@ -259,13 +298,15 @@ function detectAndReduce(input, opts) {
|
|
|
259
298
|
}
|
|
260
299
|
return node;
|
|
261
300
|
}
|
|
262
|
-
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);
|
|
301
|
+
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth);
|
|
263
302
|
const cleaned = processNode(cloned, [], 0);
|
|
264
303
|
return { cleaned, pollutedTree: polluted, pollutedKeys };
|
|
265
304
|
}
|
|
266
305
|
function sanitize(input, options = {}) {
|
|
306
|
+
validateSanitizeOptions(options);
|
|
267
307
|
const maxKeyLength = options.maxKeyLength ?? 200;
|
|
268
|
-
const
|
|
308
|
+
const maxDepthVal = options.maxDepth ?? 20;
|
|
309
|
+
const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength, maxDepthVal) : input;
|
|
269
310
|
const whitelist = normalizeWhitelist(options.whitelist);
|
|
270
311
|
const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
|
|
271
312
|
const {
|
|
@@ -288,7 +329,7 @@ function sanitize(input, options = {}) {
|
|
|
288
329
|
moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
|
|
289
330
|
return cleaned;
|
|
290
331
|
}
|
|
291
|
-
function
|
|
332
|
+
function validateSanitizeOptions(options) {
|
|
292
333
|
if (options.maxDepth !== void 0 && (typeof options.maxDepth !== "number" || options.maxDepth < 1 || options.maxDepth > 100)) {
|
|
293
334
|
throw new TypeError("maxDepth must be a number between 1 and 100");
|
|
294
335
|
}
|
|
@@ -304,6 +345,9 @@ function validateOptions(options) {
|
|
|
304
345
|
if (options.mergeStrategy !== void 0 && !["keepFirst", "keepLast", "combine"].includes(options.mergeStrategy)) {
|
|
305
346
|
throw new TypeError("mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'");
|
|
306
347
|
}
|
|
348
|
+
}
|
|
349
|
+
function validateOptions(options) {
|
|
350
|
+
validateSanitizeOptions(options);
|
|
307
351
|
if (options.sources !== void 0 && !Array.isArray(options.sources)) {
|
|
308
352
|
throw new TypeError("sources must be an array");
|
|
309
353
|
}
|
|
@@ -320,6 +364,12 @@ function validateOptions(options) {
|
|
|
320
364
|
if (options.excludePaths !== void 0 && !Array.isArray(options.excludePaths)) {
|
|
321
365
|
throw new TypeError("excludePaths must be an array");
|
|
322
366
|
}
|
|
367
|
+
if (options.logger !== void 0 && typeof options.logger !== "function") {
|
|
368
|
+
throw new TypeError("logger must be a function");
|
|
369
|
+
}
|
|
370
|
+
if (options.onPollutionDetected !== void 0 && typeof options.onPollutionDetected !== "function") {
|
|
371
|
+
throw new TypeError("onPollutionDetected must be a function");
|
|
372
|
+
}
|
|
323
373
|
}
|
|
324
374
|
function hppx(options = {}) {
|
|
325
375
|
validateOptions(options);
|
|
@@ -356,7 +406,7 @@ function hppx(options = {}) {
|
|
|
356
406
|
}
|
|
357
407
|
const part = req[source];
|
|
358
408
|
if (!isPlainObject(part)) continue;
|
|
359
|
-
const expandedPart = expandObjectPaths(part, maxKeyLength);
|
|
409
|
+
const expandedPart = expandObjectPaths(part, maxKeyLength, maxDepth);
|
|
360
410
|
const pollutedKey = `${source}Polluted`;
|
|
361
411
|
const processedKey = `__hppxProcessed_${source}`;
|
|
362
412
|
const hasProcessedBefore = Boolean(req[processedKey]);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n 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 /** Enable logging when pollution is detected (default: true) */\r\n logPollution?: boolean;\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 logPollution = true,\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 // Log pollution detection if enabled\r\n if (logPollution) {\r\n const logMessage = `[hppx] HTTP Parameter Pollution detected - ${allPollutedKeys.length} parameter(s) affected: ${allPollutedKeys.join(\", \")}`;\r\n if (logger) {\r\n try {\r\n logger(logMessage);\r\n } catch (_) {\r\n // Fallback to console.warn if logger fails\r\n console.warn(logMessage);\r\n }\r\n } else {\r\n console.warn(logMessage);\r\n }\r\n }\r\n\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":[]}
|
|
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 /** Enable logging when pollution is detected (default: true) */\r\n logPollution?: boolean;\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 /* istanbul ignore next -- defensive: callers always pass maxKeyLength explicitly */\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 maxDepth = 20,\r\n currentDepth = 0,\r\n seen?: WeakSet<object>,\r\n): Record<string, unknown> {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(obj)) return {};\r\n seenSet.add(obj);\r\n\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(\r\n value as Record<string, unknown>,\r\n maxKeyLength,\r\n maxDepth,\r\n currentDepth + 1,\r\n seenSet,\r\n )\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>(\r\n input: T,\r\n maxKeyLength?: number,\r\n maxArrayLength?: number,\r\n maxDepth = 20,\r\n currentDepth = 0,\r\n seen?: WeakSet<object>,\r\n): T {\r\n if (Array.isArray(input)) {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(input)) return [] as T;\r\n seenSet.add(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) =>\r\n safeDeepClone(v, maxKeyLength, maxArrayLength, maxDepth, currentDepth + 1, seenSet),\r\n ) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(input as object)) return {} as T;\r\n seenSet.add(input as object);\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(\r\n (input as Record<string, unknown>)[k],\r\n maxKeyLength,\r\n maxArrayLength,\r\n maxDepth,\r\n currentDepth + 1,\r\n seenSet,\r\n );\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 /* istanbul ignore next -- unreachable: strategy is validated before reaching mergeValues */\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 /* istanbul ignore if -- defensive: always called with non-empty path from walk() */\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 /* istanbul ignore next -- defensive: polluted tree keys never contain dots after expansion */\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) return opts.preserveNull ? null : undefined;\r\n if (node === undefined) return 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(\r\n polluted,\r\n path,\r\n safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth),\r\n );\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 /* istanbul ignore if -- defensive: safeDeepClone enforces the same depth limit first */\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 /* istanbul ignore if -- defensive: opts.maxKeys is always provided by callers */\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 /* istanbul ignore if -- defensive: keys already filtered by expandObjectPaths + safeDeepClone */\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, opts.maxDepth);\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 validateSanitizeOptions(options);\r\n // Normalize and expand keys prior to sanitization\r\n const maxKeyLength = options.maxKeyLength ?? 200;\r\n const maxDepthVal = options.maxDepth ?? 20;\r\n const expandedInput = isPlainObject(input)\r\n ? expandObjectPaths(input, maxKeyLength, maxDepthVal)\r\n : 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 validateSanitizeOptions(options: SanitizeOptions): 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}\r\n\r\nfunction validateOptions(options: HppxOptions): void {\r\n validateSanitizeOptions(options);\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 if (options.logger !== undefined && typeof options.logger !== \"function\") {\r\n throw new TypeError(\"logger must be a function\");\r\n }\r\n if (\r\n options.onPollutionDetected !== undefined &&\r\n typeof options.onPollutionDetected !== \"function\"\r\n ) {\r\n throw new TypeError(\"onPollutionDetected must be a function\");\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 logPollution = true,\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, maxDepth);\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 // Log pollution detection if enabled\r\n if (logPollution) {\r\n const logMessage = `[hppx] HTTP Parameter Pollution detected - ${allPollutedKeys.length} parameter(s) affected: ${allPollutedKeys.join(\", \")}`;\r\n if (logger) {\r\n try {\r\n logger(logMessage);\r\n } catch (_) {\r\n // Fallback to console.warn if logger fails\r\n console.warn(logMessage);\r\n }\r\n } else {\r\n console.warn(logMessage);\r\n }\r\n }\r\n\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.mjs
CHANGED
|
@@ -27,13 +27,25 @@ function parsePathSegments(key) {
|
|
|
27
27
|
}
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
|
-
function expandObjectPaths(obj, maxKeyLength) {
|
|
30
|
+
function expandObjectPaths(obj, maxKeyLength, maxDepth = 20, currentDepth = 0, seen) {
|
|
31
|
+
if (currentDepth > maxDepth) {
|
|
32
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
33
|
+
}
|
|
34
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
35
|
+
if (seenSet.has(obj)) return {};
|
|
36
|
+
seenSet.add(obj);
|
|
31
37
|
const result = {};
|
|
32
38
|
for (const rawKey of Object.keys(obj)) {
|
|
33
39
|
const safeKey = sanitizeKey(rawKey, maxKeyLength);
|
|
34
40
|
if (!safeKey) continue;
|
|
35
41
|
const value = obj[rawKey];
|
|
36
|
-
const expandedValue = isPlainObject(value) ? expandObjectPaths(
|
|
42
|
+
const expandedValue = isPlainObject(value) ? expandObjectPaths(
|
|
43
|
+
value,
|
|
44
|
+
maxKeyLength,
|
|
45
|
+
maxDepth,
|
|
46
|
+
currentDepth + 1,
|
|
47
|
+
seenSet
|
|
48
|
+
) : value;
|
|
37
49
|
if (safeKey.includes(".") || safeKey.includes("[")) {
|
|
38
50
|
const segments = parsePathSegments(safeKey);
|
|
39
51
|
if (segments.length > 0) {
|
|
@@ -67,17 +79,38 @@ function setReqPropertySafe(target, key, value) {
|
|
|
67
79
|
} catch (_) {
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
|
-
function safeDeepClone(input, maxKeyLength, maxArrayLength) {
|
|
82
|
+
function safeDeepClone(input, maxKeyLength, maxArrayLength, maxDepth = 20, currentDepth = 0, seen) {
|
|
71
83
|
if (Array.isArray(input)) {
|
|
84
|
+
if (currentDepth > maxDepth) {
|
|
85
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
86
|
+
}
|
|
87
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
88
|
+
if (seenSet.has(input)) return [];
|
|
89
|
+
seenSet.add(input);
|
|
72
90
|
const limit = maxArrayLength ?? 1e3;
|
|
73
91
|
const limited = input.slice(0, limit);
|
|
74
|
-
return limited.map(
|
|
92
|
+
return limited.map(
|
|
93
|
+
(v) => safeDeepClone(v, maxKeyLength, maxArrayLength, maxDepth, currentDepth + 1, seenSet)
|
|
94
|
+
);
|
|
75
95
|
}
|
|
76
96
|
if (isPlainObject(input)) {
|
|
97
|
+
if (currentDepth > maxDepth) {
|
|
98
|
+
throw new Error(`Maximum object depth (${maxDepth}) exceeded`);
|
|
99
|
+
}
|
|
100
|
+
const seenSet = seen ?? /* @__PURE__ */ new WeakSet();
|
|
101
|
+
if (seenSet.has(input)) return {};
|
|
102
|
+
seenSet.add(input);
|
|
77
103
|
const out = {};
|
|
78
104
|
for (const k of Object.keys(input)) {
|
|
79
105
|
if (!sanitizeKey(k, maxKeyLength)) continue;
|
|
80
|
-
out[k] = safeDeepClone(
|
|
106
|
+
out[k] = safeDeepClone(
|
|
107
|
+
input[k],
|
|
108
|
+
maxKeyLength,
|
|
109
|
+
maxArrayLength,
|
|
110
|
+
maxDepth,
|
|
111
|
+
currentDepth + 1,
|
|
112
|
+
seenSet
|
|
113
|
+
);
|
|
81
114
|
}
|
|
82
115
|
return out;
|
|
83
116
|
}
|
|
@@ -95,6 +128,7 @@ function mergeValues(values, strategy) {
|
|
|
95
128
|
else acc.push(v);
|
|
96
129
|
return acc;
|
|
97
130
|
}, []);
|
|
131
|
+
/* istanbul ignore next -- unreachable: strategy is validated before reaching mergeValues */
|
|
98
132
|
default:
|
|
99
133
|
return values[values.length - 1];
|
|
100
134
|
}
|
|
@@ -197,7 +231,8 @@ function detectAndReduce(input, opts) {
|
|
|
197
231
|
const polluted = {};
|
|
198
232
|
const pollutedKeys = [];
|
|
199
233
|
function processNode(node, path = [], depth = 0) {
|
|
200
|
-
if (node === null
|
|
234
|
+
if (node === null) return opts.preserveNull ? null : void 0;
|
|
235
|
+
if (node === void 0) return node;
|
|
201
236
|
if (Array.isArray(node)) {
|
|
202
237
|
const limit = opts.maxArrayLength ?? 1e3;
|
|
203
238
|
const limitedNode = node.slice(0, limit);
|
|
@@ -205,7 +240,11 @@ function detectAndReduce(input, opts) {
|
|
|
205
240
|
if (opts.mergeStrategy === "combine") {
|
|
206
241
|
return mergeValues(mapped, "combine");
|
|
207
242
|
}
|
|
208
|
-
setIn(
|
|
243
|
+
setIn(
|
|
244
|
+
polluted,
|
|
245
|
+
path,
|
|
246
|
+
safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth)
|
|
247
|
+
);
|
|
209
248
|
pollutedKeys.push(path.join("."));
|
|
210
249
|
const reduced = mergeValues(mapped, opts.mergeStrategy);
|
|
211
250
|
return reduced;
|
|
@@ -231,13 +270,15 @@ function detectAndReduce(input, opts) {
|
|
|
231
270
|
}
|
|
232
271
|
return node;
|
|
233
272
|
}
|
|
234
|
-
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength);
|
|
273
|
+
const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth);
|
|
235
274
|
const cleaned = processNode(cloned, [], 0);
|
|
236
275
|
return { cleaned, pollutedTree: polluted, pollutedKeys };
|
|
237
276
|
}
|
|
238
277
|
function sanitize(input, options = {}) {
|
|
278
|
+
validateSanitizeOptions(options);
|
|
239
279
|
const maxKeyLength = options.maxKeyLength ?? 200;
|
|
240
|
-
const
|
|
280
|
+
const maxDepthVal = options.maxDepth ?? 20;
|
|
281
|
+
const expandedInput = isPlainObject(input) ? expandObjectPaths(input, maxKeyLength, maxDepthVal) : input;
|
|
241
282
|
const whitelist = normalizeWhitelist(options.whitelist);
|
|
242
283
|
const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
|
|
243
284
|
const {
|
|
@@ -260,7 +301,7 @@ function sanitize(input, options = {}) {
|
|
|
260
301
|
moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
|
|
261
302
|
return cleaned;
|
|
262
303
|
}
|
|
263
|
-
function
|
|
304
|
+
function validateSanitizeOptions(options) {
|
|
264
305
|
if (options.maxDepth !== void 0 && (typeof options.maxDepth !== "number" || options.maxDepth < 1 || options.maxDepth > 100)) {
|
|
265
306
|
throw new TypeError("maxDepth must be a number between 1 and 100");
|
|
266
307
|
}
|
|
@@ -276,6 +317,9 @@ function validateOptions(options) {
|
|
|
276
317
|
if (options.mergeStrategy !== void 0 && !["keepFirst", "keepLast", "combine"].includes(options.mergeStrategy)) {
|
|
277
318
|
throw new TypeError("mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'");
|
|
278
319
|
}
|
|
320
|
+
}
|
|
321
|
+
function validateOptions(options) {
|
|
322
|
+
validateSanitizeOptions(options);
|
|
279
323
|
if (options.sources !== void 0 && !Array.isArray(options.sources)) {
|
|
280
324
|
throw new TypeError("sources must be an array");
|
|
281
325
|
}
|
|
@@ -292,6 +336,12 @@ function validateOptions(options) {
|
|
|
292
336
|
if (options.excludePaths !== void 0 && !Array.isArray(options.excludePaths)) {
|
|
293
337
|
throw new TypeError("excludePaths must be an array");
|
|
294
338
|
}
|
|
339
|
+
if (options.logger !== void 0 && typeof options.logger !== "function") {
|
|
340
|
+
throw new TypeError("logger must be a function");
|
|
341
|
+
}
|
|
342
|
+
if (options.onPollutionDetected !== void 0 && typeof options.onPollutionDetected !== "function") {
|
|
343
|
+
throw new TypeError("onPollutionDetected must be a function");
|
|
344
|
+
}
|
|
295
345
|
}
|
|
296
346
|
function hppx(options = {}) {
|
|
297
347
|
validateOptions(options);
|
|
@@ -328,7 +378,7 @@ function hppx(options = {}) {
|
|
|
328
378
|
}
|
|
329
379
|
const part = req[source];
|
|
330
380
|
if (!isPlainObject(part)) continue;
|
|
331
|
-
const expandedPart = expandObjectPaths(part, maxKeyLength);
|
|
381
|
+
const expandedPart = expandObjectPaths(part, maxKeyLength, maxDepth);
|
|
332
382
|
const pollutedKey = `${source}Polluted`;
|
|
333
383
|
const processedKey = `__hppxProcessed_${source}`;
|
|
334
384
|
const hasProcessedBefore = Boolean(req[processedKey]);
|
package/dist/index.mjs.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 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 /** Enable logging when pollution is detected (default: true) */\r\n logPollution?: boolean;\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 logPollution = true,\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 // Log pollution detection if enabled\r\n if (logPollution) {\r\n const logMessage = `[hppx] HTTP Parameter Pollution detected - ${allPollutedKeys.length} parameter(s) affected: ${allPollutedKeys.join(\", \")}`;\r\n if (logger) {\r\n try {\r\n logger(logMessage);\r\n } catch (_) {\r\n // Fallback to console.warn if logger fails\r\n console.warn(logMessage);\r\n }\r\n } else {\r\n console.warn(logMessage);\r\n }\r\n }\r\n\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":[]}
|
|
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 /** Enable logging when pollution is detected (default: true) */\r\n logPollution?: boolean;\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 /* istanbul ignore next -- defensive: callers always pass maxKeyLength explicitly */\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 maxDepth = 20,\r\n currentDepth = 0,\r\n seen?: WeakSet<object>,\r\n): Record<string, unknown> {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(obj)) return {};\r\n seenSet.add(obj);\r\n\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(\r\n value as Record<string, unknown>,\r\n maxKeyLength,\r\n maxDepth,\r\n currentDepth + 1,\r\n seenSet,\r\n )\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>(\r\n input: T,\r\n maxKeyLength?: number,\r\n maxArrayLength?: number,\r\n maxDepth = 20,\r\n currentDepth = 0,\r\n seen?: WeakSet<object>,\r\n): T {\r\n if (Array.isArray(input)) {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(input)) return [] as T;\r\n seenSet.add(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) =>\r\n safeDeepClone(v, maxKeyLength, maxArrayLength, maxDepth, currentDepth + 1, seenSet),\r\n ) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n if (currentDepth > maxDepth) {\r\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\r\n }\r\n const seenSet = seen ?? new WeakSet<object>();\r\n if (seenSet.has(input as object)) return {} as T;\r\n seenSet.add(input as object);\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(\r\n (input as Record<string, unknown>)[k],\r\n maxKeyLength,\r\n maxArrayLength,\r\n maxDepth,\r\n currentDepth + 1,\r\n seenSet,\r\n );\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 /* istanbul ignore next -- unreachable: strategy is validated before reaching mergeValues */\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 /* istanbul ignore if -- defensive: always called with non-empty path from walk() */\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 /* istanbul ignore next -- defensive: polluted tree keys never contain dots after expansion */\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) return opts.preserveNull ? null : undefined;\r\n if (node === undefined) return 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(\r\n polluted,\r\n path,\r\n safeDeepClone(limitedNode, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth),\r\n );\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 /* istanbul ignore if -- defensive: safeDeepClone enforces the same depth limit first */\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 /* istanbul ignore if -- defensive: opts.maxKeys is always provided by callers */\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 /* istanbul ignore if -- defensive: keys already filtered by expandObjectPaths + safeDeepClone */\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, opts.maxDepth);\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 validateSanitizeOptions(options);\r\n // Normalize and expand keys prior to sanitization\r\n const maxKeyLength = options.maxKeyLength ?? 200;\r\n const maxDepthVal = options.maxDepth ?? 20;\r\n const expandedInput = isPlainObject(input)\r\n ? expandObjectPaths(input, maxKeyLength, maxDepthVal)\r\n : 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 validateSanitizeOptions(options: SanitizeOptions): 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}\r\n\r\nfunction validateOptions(options: HppxOptions): void {\r\n validateSanitizeOptions(options);\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 if (options.logger !== undefined && typeof options.logger !== \"function\") {\r\n throw new TypeError(\"logger must be a function\");\r\n }\r\n if (\r\n options.onPollutionDetected !== undefined &&\r\n typeof options.onPollutionDetected !== \"function\"\r\n ) {\r\n throw new TypeError(\"onPollutionDetected must be a function\");\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 logPollution = true,\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, maxDepth);\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 // Log pollution detection if enabled\r\n if (logPollution) {\r\n const logMessage = `[hppx] HTTP Parameter Pollution detected - ${allPollutedKeys.length} parameter(s) affected: ${allPollutedKeys.join(\", \")}`;\r\n if (logger) {\r\n try {\r\n logger(logMessage);\r\n } catch (_) {\r\n // Fallback to console.warn if logger fails\r\n console.warn(logMessage);\r\n }\r\n } else {\r\n console.warn(logMessage);\r\n }\r\n }\r\n\r\n if (onPollutionDetected) {\r\n try {\r\n // Determine which sources had pollution\r\n for (const source of sources) {\r\n const pollutedKey = `${source}Polluted`;\r\n const pollutedData = req[pollutedKey];\r\n if (pollutedData && Object.keys(pollutedData).length > 0) {\r\n const sourcePollutedKeys = allPollutedKeys.filter((k) =>\r\n k.startsWith(`${source}.`),\r\n );\r\n if (sourcePollutedKeys.length > 0) {\r\n onPollutionDetected(req, {\r\n source: source,\r\n pollutedKeys: sourcePollutedKeys,\r\n });\r\n }\r\n }\r\n }\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n // Enhanced error handling with detailed logging\r\n const error = err instanceof Error ? err : new Error(String(err));\r\n\r\n if (logger) {\r\n try {\r\n logger(error);\r\n } catch (logErr) {\r\n // If custom logger fails, use console.error as fallback in development\r\n if (process.env.NODE_ENV !== \"production\") {\r\n console.error(\"[hppx] Logger failed:\", logErr);\r\n console.error(\"[hppx] Original error:\", error);\r\n }\r\n }\r\n }\r\n\r\n // Pass error to next middleware for proper error handling\r\n return next(error);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hppx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
"types": "dist/index.d.ts",
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
30
|
},
|
|
26
31
|
"files": [
|
package/src/index.d.cts
CHANGED
|
File without changes
|