hppx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hiprax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # hppx
2
+
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
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
8
+
9
+ ## Features
10
+
11
+ - Array merging strategies: `keepFirst`, `keepLast` (default), `combine`
12
+ - Safe-by-default: blocks `__proto__`, `prototype`, `constructor`
13
+ - Nested whitelist with dot-notation and leaf matching
14
+ - Records polluted parameters on the request (`queryPolluted`, `bodyPolluted`, `paramsPolluted`)
15
+ - Works with multiple middlewares on different routes (whitelists applied incrementally)
16
+ - DoS-guards: `maxDepth`, `maxKeys`
17
+ - Fully typed API and helpers (`sanitize`)
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install hppx
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import express from "express";
29
+ import hppx from "hppx";
30
+
31
+ const app = express();
32
+ app.use(express.urlencoded({ extended: true }));
33
+ app.use(express.json());
34
+
35
+ app.use(
36
+ hppx({
37
+ whitelist: ["tags", "user.roles", "ids"],
38
+ mergeStrategy: "keepLast",
39
+ sources: ["query", "body"],
40
+ })
41
+ );
42
+
43
+ app.get("/search", (req, res) => {
44
+ res.json({
45
+ query: req.query,
46
+ queryPolluted: req.queryPolluted ?? {},
47
+ body: req.body ?? {},
48
+ bodyPolluted: req.bodyPolluted ?? {},
49
+ });
50
+ });
51
+ ```
52
+
53
+ ## API
54
+
55
+ ### default export: `hppx(options?: HppxOptions)`
56
+
57
+ Creates an Express-compatible middleware. Applies sanitization to each selected source and exposes `*.Polluted` objects.
58
+
59
+ Key options:
60
+
61
+ - `whitelist?: string[]` — keys allowed as arrays; supports dot-notation; leaf matches too
62
+ - `mergeStrategy?: 'keepFirst'|'keepLast'|'combine'` — how to reduce arrays when not whitelisted
63
+ - `sources?: Array<'query'|'body'|'params'>` — which request parts to sanitize
64
+ - `checkBodyContentType?: 'urlencoded'|'any'|'none'` — when to process `req.body` (default: `urlencoded`)
65
+ - `excludePaths?: string[]` — exclude specific paths (supports `*` suffix)
66
+ - `maxDepth?: number` and `maxKeys?: number` — DoS protections
67
+ - `strict?: boolean` — if pollution detected, immediately respond 400
68
+ - `onPollutionDetected?: (req, info) => void` — callback on detection
69
+
70
+ ### named export: `sanitize(input, options)`
71
+
72
+ Sanitize an arbitrary object using the same rules as the middleware. Useful for manual usage.
73
+
74
+ ## Advanced usage
75
+
76
+ - Strict mode (respond 400 on pollution):
77
+
78
+ ```ts
79
+ app.use(hppx({ strict: true }));
80
+ ```
81
+
82
+ - Process JSON bodies too:
83
+
84
+ ```ts
85
+ app.use(express.json());
86
+ app.use(hppx({ checkBodyContentType: 'any' }));
87
+ ```
88
+
89
+ - Exclude specific paths (supports `*` suffix):
90
+
91
+ ```ts
92
+ app.use(hppx({ excludePaths: ['/public', '/assets*'] }));
93
+ ```
94
+
95
+ - Use the sanitizer directly:
96
+
97
+ ```ts
98
+ import { sanitize } from 'hppx';
99
+
100
+ const clean = sanitize(payload, {
101
+ whitelist: ['user.tags'],
102
+ mergeStrategy: 'keepFirst',
103
+ });
104
+ ```
105
+
106
+ ## Notes
107
+
108
+ - Arrays are reduced by default; whitelisted paths are preserved as arrays.
109
+ - Dangerous keys like `__proto__`, `prototype`, `constructor` are removed.
110
+ - DoS protections are available via `maxDepth` and `maxKeys`.
111
+
112
+ ## 📄 License
113
+
114
+ MIT License - see [LICENSE](LICENSE) file for details.
115
+
116
+ ## 🔗 Links
117
+
118
+ - [NPM Package](https://www.npmjs.com/package/@hiprax/hppx)
119
+ - [GitHub Repository](https://github.com/Hiprax/hppx)
120
+ - [Issue Tracker](https://github.com/Hiprax/hppx/issues)
121
+
122
+ ---
123
+
124
+ ### **Made with ❤️ for secure applications**
package/dist/index.cjs ADDED
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DANGEROUS_KEYS: () => DANGEROUS_KEYS,
24
+ DEFAULT_SOURCES: () => DEFAULT_SOURCES,
25
+ DEFAULT_STRATEGY: () => DEFAULT_STRATEGY,
26
+ default: () => hppx,
27
+ sanitize: () => sanitize
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var DEFAULT_SOURCES = ["query", "body", "params"];
31
+ var DEFAULT_STRATEGY = "keepLast";
32
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
33
+ function isPlainObject(value) {
34
+ if (value === null || typeof value !== "object") return false;
35
+ const proto = Object.getPrototypeOf(value);
36
+ return proto === Object.prototype || proto === null;
37
+ }
38
+ function sanitizeKey(key) {
39
+ if (typeof key !== "string") return null;
40
+ if (DANGEROUS_KEYS.has(key)) return null;
41
+ if (key.includes("\0")) return null;
42
+ return key;
43
+ }
44
+ function parsePathSegments(key) {
45
+ const dotted = key.replace(/\]/g, "").replace(/\[/g, ".");
46
+ return dotted.split(".").filter((s) => s.length > 0);
47
+ }
48
+ function expandObjectPaths(obj) {
49
+ const result = {};
50
+ for (const rawKey of Object.keys(obj)) {
51
+ const safeKey = sanitizeKey(rawKey);
52
+ if (!safeKey) continue;
53
+ const value = obj[rawKey];
54
+ const expandedValue = isPlainObject(value) ? expandObjectPaths(value) : value;
55
+ if (safeKey.includes(".") || safeKey.includes("[")) {
56
+ const segments = parsePathSegments(safeKey);
57
+ if (segments.length > 0) {
58
+ setIn(result, segments, expandedValue);
59
+ continue;
60
+ }
61
+ }
62
+ result[safeKey] = expandedValue;
63
+ }
64
+ return result;
65
+ }
66
+ function setReqPropertySafe(target, key, value) {
67
+ try {
68
+ const desc = Object.getOwnPropertyDescriptor(target, key);
69
+ if (desc && desc.configurable === false && desc.writable === false) {
70
+ return;
71
+ }
72
+ if (!desc || desc.configurable !== false) {
73
+ Object.defineProperty(target, key, {
74
+ value,
75
+ writable: true,
76
+ configurable: true,
77
+ enumerable: true
78
+ });
79
+ return;
80
+ }
81
+ } catch (_) {
82
+ }
83
+ try {
84
+ target[key] = value;
85
+ } catch (_) {
86
+ }
87
+ }
88
+ function safeDeepClone(input) {
89
+ if (Array.isArray(input)) {
90
+ return input.map((v) => safeDeepClone(v));
91
+ }
92
+ if (isPlainObject(input)) {
93
+ const out = {};
94
+ for (const k of Object.keys(input)) {
95
+ if (!sanitizeKey(k)) continue;
96
+ out[k] = safeDeepClone(input[k]);
97
+ }
98
+ return out;
99
+ }
100
+ return input;
101
+ }
102
+ function mergeValues(values, strategy) {
103
+ switch (strategy) {
104
+ case "keepFirst":
105
+ return values[0];
106
+ case "keepLast":
107
+ return values[values.length - 1];
108
+ case "combine":
109
+ return values.reduce((acc, v) => {
110
+ if (Array.isArray(v)) acc.push(...v);
111
+ else acc.push(v);
112
+ return acc;
113
+ }, []);
114
+ default:
115
+ return values[values.length - 1];
116
+ }
117
+ }
118
+ function isUrlEncodedContentType(req) {
119
+ const ct = String(req?.headers?.["content-type"] || "").toLowerCase();
120
+ return ct.startsWith("application/x-www-form-urlencoded");
121
+ }
122
+ function shouldExcludePath(path, excludePaths) {
123
+ if (!path || excludePaths.length === 0) return false;
124
+ const currentPath = path;
125
+ for (const p of excludePaths) {
126
+ if (p.endsWith("*")) {
127
+ if (currentPath.startsWith(p.slice(0, -1))) return true;
128
+ } else if (currentPath === p) {
129
+ return true;
130
+ }
131
+ }
132
+ return false;
133
+ }
134
+ function normalizeWhitelist(whitelist) {
135
+ if (!whitelist) return [];
136
+ if (typeof whitelist === "string") return [whitelist];
137
+ return whitelist.filter((w) => typeof w === "string");
138
+ }
139
+ function buildWhitelistHelpers(whitelist) {
140
+ const exact = new Set(whitelist);
141
+ const prefixes = whitelist.filter((w) => w.length > 0);
142
+ return {
143
+ exact,
144
+ prefixes,
145
+ isWhitelistedPath(pathParts) {
146
+ if (pathParts.length === 0) return false;
147
+ const full = pathParts.join(".");
148
+ if (exact.has(full)) return true;
149
+ const leaf = pathParts[pathParts.length - 1];
150
+ if (exact.has(leaf)) return true;
151
+ for (const p of prefixes) {
152
+ if (full === p || full.startsWith(p + ".")) return true;
153
+ }
154
+ return false;
155
+ }
156
+ };
157
+ }
158
+ function setIn(target, path, value) {
159
+ if (path.length === 0) {
160
+ return;
161
+ }
162
+ let cur = target;
163
+ for (let i = 0; i < path.length - 1; i++) {
164
+ const k = path[i];
165
+ if (!isPlainObject(cur[k])) cur[k] = {};
166
+ cur = cur[k];
167
+ }
168
+ const lastKey = path[path.length - 1];
169
+ cur[lastKey] = value;
170
+ }
171
+ function moveWhitelistedFromPolluted(reqPart, polluted, isWhitelisted) {
172
+ function walk(node, path = [], parent) {
173
+ for (const k of Object.keys(node)) {
174
+ const v = node[k];
175
+ const curPath = [...path, k];
176
+ if (isPlainObject(v)) {
177
+ walk(v, curPath, node);
178
+ if (Object.keys(v).length === 0) {
179
+ delete node[k];
180
+ }
181
+ } else {
182
+ if (isWhitelisted(curPath)) {
183
+ const normalizedPath = curPath.flatMap(
184
+ (seg) => seg.includes(".") ? seg.split(".") : [seg]
185
+ );
186
+ setIn(reqPart, normalizedPath, v);
187
+ delete node[k];
188
+ }
189
+ }
190
+ }
191
+ }
192
+ walk(polluted);
193
+ }
194
+ function detectAndReduce(input, opts) {
195
+ let keyCount = 0;
196
+ const polluted = {};
197
+ const pollutedKeys = [];
198
+ function processNode(node, path = [], depth = 0) {
199
+ if (node === null || node === void 0) return opts.preserveNull ? node : node;
200
+ if (Array.isArray(node)) {
201
+ const mapped = node.map((v) => processNode(v, path, depth));
202
+ if (opts.mergeStrategy === "combine") {
203
+ return mergeValues(mapped, "combine");
204
+ }
205
+ setIn(polluted, path, safeDeepClone(node));
206
+ pollutedKeys.push(path.join("."));
207
+ const reduced = mergeValues(mapped, opts.mergeStrategy);
208
+ return reduced;
209
+ }
210
+ if (isPlainObject(node)) {
211
+ if (depth > opts.maxDepth)
212
+ throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);
213
+ const out = {};
214
+ for (const rawKey of Object.keys(node)) {
215
+ keyCount++;
216
+ if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {
217
+ throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);
218
+ }
219
+ const safeKey = sanitizeKey(rawKey);
220
+ if (!safeKey) continue;
221
+ const child = node[rawKey];
222
+ const childPath = path.concat([safeKey]);
223
+ let value = processNode(child, childPath, depth + 1);
224
+ if (typeof value === "string" && opts.trimValues) value = value.trim();
225
+ out[safeKey] = value;
226
+ }
227
+ return out;
228
+ }
229
+ return node;
230
+ }
231
+ const cloned = safeDeepClone(input);
232
+ const cleaned = processNode(cloned, [], 0);
233
+ return { cleaned, pollutedTree: polluted, pollutedKeys };
234
+ }
235
+ function sanitize(input, options = {}) {
236
+ const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;
237
+ const whitelist = normalizeWhitelist(options.whitelist);
238
+ const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
239
+ const {
240
+ mergeStrategy = DEFAULT_STRATEGY,
241
+ maxDepth = 20,
242
+ maxKeys = 5e3,
243
+ trimValues = false,
244
+ preserveNull = true
245
+ } = options;
246
+ const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {
247
+ mergeStrategy,
248
+ maxDepth,
249
+ maxKeys,
250
+ trimValues,
251
+ preserveNull
252
+ });
253
+ moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
254
+ return cleaned;
255
+ }
256
+ function hppx(options = {}) {
257
+ const {
258
+ whitelist = [],
259
+ mergeStrategy = DEFAULT_STRATEGY,
260
+ sources = DEFAULT_SOURCES,
261
+ checkBodyContentType = "urlencoded",
262
+ excludePaths = [],
263
+ maxDepth = 20,
264
+ maxKeys = 5e3,
265
+ trimValues = false,
266
+ preserveNull = true,
267
+ strict = false,
268
+ onPollutionDetected,
269
+ logger
270
+ } = options;
271
+ const whitelistArr = normalizeWhitelist(whitelist);
272
+ const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);
273
+ return function hppxMiddleware(req, res, next) {
274
+ try {
275
+ if (shouldExcludePath(req?.path, excludePaths)) return next();
276
+ let anyPollutionDetected = false;
277
+ const allPollutedKeys = [];
278
+ for (const source of sources) {
279
+ if (!req || typeof req !== "object") break;
280
+ if (req[source] === void 0) continue;
281
+ if (source === "body") {
282
+ if (checkBodyContentType === "none") continue;
283
+ if (checkBodyContentType === "urlencoded" && !isUrlEncodedContentType(req)) continue;
284
+ }
285
+ const part = req[source];
286
+ if (!isPlainObject(part)) continue;
287
+ const expandedPart = expandObjectPaths(part);
288
+ const pollutedKey = `${source}Polluted`;
289
+ const processedKey = `__hppxProcessed_${source}`;
290
+ const hasProcessedBefore = Boolean(req[processedKey]);
291
+ if (!hasProcessedBefore) {
292
+ const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {
293
+ mergeStrategy,
294
+ maxDepth,
295
+ maxKeys,
296
+ trimValues,
297
+ preserveNull
298
+ });
299
+ setReqPropertySafe(req, source, cleaned);
300
+ setReqPropertySafe(req, pollutedKey, pollutedTree);
301
+ req[processedKey] = true;
302
+ moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
303
+ if (pollutedKeys.length > 0) {
304
+ anyPollutionDetected = true;
305
+ for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);
306
+ }
307
+ } else {
308
+ moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
309
+ }
310
+ }
311
+ if (anyPollutionDetected) {
312
+ if (onPollutionDetected) {
313
+ try {
314
+ onPollutionDetected(req, {
315
+ source: "query",
316
+ pollutedKeys: allPollutedKeys
317
+ });
318
+ } catch (_) {
319
+ }
320
+ }
321
+ if (strict && res && typeof res.status === "function") {
322
+ return res.status(400).json({
323
+ error: "Bad Request",
324
+ message: "HTTP Parameter Pollution detected",
325
+ pollutedParameters: allPollutedKeys,
326
+ code: "HPP_DETECTED"
327
+ });
328
+ }
329
+ }
330
+ return next();
331
+ } catch (err) {
332
+ if (logger) {
333
+ try {
334
+ logger(err);
335
+ } catch (_) {
336
+ }
337
+ }
338
+ return next(err);
339
+ }
340
+ };
341
+ }
342
+ // Annotate the CommonJS export names for ESM import in node:
343
+ 0 && (module.exports = {
344
+ DANGEROUS_KEYS,
345
+ DEFAULT_SOURCES,
346
+ DEFAULT_STRATEGY,
347
+ sanitize
348
+ });
349
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (req: any, info: { source: RequestSource; pollutedKeys: string[] }) => void;\r\n logger?: (err: unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n return key;\r\n}\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n return dotted.split(\".\").filter((s) => s.length > 0);\r\n}\r\n\r\nfunction expandObjectPaths(obj: Record<string, unknown>): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const value = (obj as any)[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: any, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T): T {\r\n if (Array.isArray(input)) {\r\n return input.map((v) => safeDeepClone(v)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k]);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n if (exact.has(full)) return true;\r\n // leaf match\r\n const leaf = pathParts[pathParts.length - 1]!;\r\n if (exact.has(leaf)) return true;\r\n // prefix match (treat any listed segment as prefix of a subtree)\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) return true;\r\n }\r\n return false;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: any = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n if (!isPlainObject(cur[k])) cur[k] = {};\r\n cur = cur[k];\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(\r\n node: Record<string, unknown>,\r\n path: string[] = [],\r\n parent?: Record<string, unknown>,\r\n ) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath, node);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete (node as any)[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete (node as any)[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<SanitizeOptions, \"mergeStrategy\" | \"maxDepth\" | \"maxKeys\" | \"trimValues\" | \"preserveNull\">\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n const mapped = node.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(node));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: any) => void;\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */ if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean((req as any)[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n (req as any)[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n onPollutionDetected(req, {\r\n source: \"query\",\r\n pollutedKeys: allPollutedKeys,\r\n });\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n if (logger) {\r\n try {\r\n logger(err);\r\n } catch (_) {\r\n /* noop */\r\n }\r\n }\r\n return next(err);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * hppx — Superior HTTP Parameter Pollution protection middleware
3
+ *
4
+ * - Protects against parameter and prototype pollution
5
+ * - Supports nested whitelists via dot-notation and leaf matching
6
+ * - Merge strategies: keepFirst | keepLast | combine
7
+ * - Multiple middleware compatibility: arrays are "put aside" once and selectively restored
8
+ * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted
9
+ * - TypeScript-first API
10
+ */
11
+ type RequestSource = "query" | "body" | "params";
12
+ type MergeStrategy = "keepFirst" | "keepLast" | "combine";
13
+ interface SanitizeOptions {
14
+ whitelist?: string[] | string;
15
+ mergeStrategy?: MergeStrategy;
16
+ maxDepth?: number;
17
+ maxKeys?: number;
18
+ trimValues?: boolean;
19
+ preserveNull?: boolean;
20
+ }
21
+ interface HppxOptions extends SanitizeOptions {
22
+ sources?: RequestSource[];
23
+ /** When to process req.body */
24
+ checkBodyContentType?: "urlencoded" | "any" | "none";
25
+ excludePaths?: string[];
26
+ strict?: boolean;
27
+ onPollutionDetected?: (req: any, info: {
28
+ source: RequestSource;
29
+ pollutedKeys: string[];
30
+ }) => void;
31
+ logger?: (err: unknown) => void;
32
+ }
33
+ interface SanitizedResult<T> {
34
+ cleaned: T;
35
+ pollutedTree: Record<string, unknown>;
36
+ pollutedKeys: string[];
37
+ }
38
+ declare const DEFAULT_SOURCES: RequestSource[];
39
+ declare const DEFAULT_STRATEGY: MergeStrategy;
40
+ declare const DANGEROUS_KEYS: Set<string>;
41
+ declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
42
+ type ExpressLikeNext = (err?: any) => void;
43
+ declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
44
+
45
+ export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, hppx as default, sanitize };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * hppx — Superior HTTP Parameter Pollution protection middleware
3
+ *
4
+ * - Protects against parameter and prototype pollution
5
+ * - Supports nested whitelists via dot-notation and leaf matching
6
+ * - Merge strategies: keepFirst | keepLast | combine
7
+ * - Multiple middleware compatibility: arrays are "put aside" once and selectively restored
8
+ * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted
9
+ * - TypeScript-first API
10
+ */
11
+ type RequestSource = "query" | "body" | "params";
12
+ type MergeStrategy = "keepFirst" | "keepLast" | "combine";
13
+ interface SanitizeOptions {
14
+ whitelist?: string[] | string;
15
+ mergeStrategy?: MergeStrategy;
16
+ maxDepth?: number;
17
+ maxKeys?: number;
18
+ trimValues?: boolean;
19
+ preserveNull?: boolean;
20
+ }
21
+ interface HppxOptions extends SanitizeOptions {
22
+ sources?: RequestSource[];
23
+ /** When to process req.body */
24
+ checkBodyContentType?: "urlencoded" | "any" | "none";
25
+ excludePaths?: string[];
26
+ strict?: boolean;
27
+ onPollutionDetected?: (req: any, info: {
28
+ source: RequestSource;
29
+ pollutedKeys: string[];
30
+ }) => void;
31
+ logger?: (err: unknown) => void;
32
+ }
33
+ interface SanitizedResult<T> {
34
+ cleaned: T;
35
+ pollutedTree: Record<string, unknown>;
36
+ pollutedKeys: string[];
37
+ }
38
+ declare const DEFAULT_SOURCES: RequestSource[];
39
+ declare const DEFAULT_STRATEGY: MergeStrategy;
40
+ declare const DANGEROUS_KEYS: Set<string>;
41
+ declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
42
+ type ExpressLikeNext = (err?: any) => void;
43
+ declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
44
+
45
+ export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, hppx as default, sanitize };
package/dist/index.js ADDED
@@ -0,0 +1,321 @@
1
+ // src/index.ts
2
+ var DEFAULT_SOURCES = ["query", "body", "params"];
3
+ var DEFAULT_STRATEGY = "keepLast";
4
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
5
+ function isPlainObject(value) {
6
+ if (value === null || typeof value !== "object") return false;
7
+ const proto = Object.getPrototypeOf(value);
8
+ return proto === Object.prototype || proto === null;
9
+ }
10
+ function sanitizeKey(key) {
11
+ if (typeof key !== "string") return null;
12
+ if (DANGEROUS_KEYS.has(key)) return null;
13
+ if (key.includes("\0")) return null;
14
+ return key;
15
+ }
16
+ function parsePathSegments(key) {
17
+ const dotted = key.replace(/\]/g, "").replace(/\[/g, ".");
18
+ return dotted.split(".").filter((s) => s.length > 0);
19
+ }
20
+ function expandObjectPaths(obj) {
21
+ const result = {};
22
+ for (const rawKey of Object.keys(obj)) {
23
+ const safeKey = sanitizeKey(rawKey);
24
+ if (!safeKey) continue;
25
+ const value = obj[rawKey];
26
+ const expandedValue = isPlainObject(value) ? expandObjectPaths(value) : value;
27
+ if (safeKey.includes(".") || safeKey.includes("[")) {
28
+ const segments = parsePathSegments(safeKey);
29
+ if (segments.length > 0) {
30
+ setIn(result, segments, expandedValue);
31
+ continue;
32
+ }
33
+ }
34
+ result[safeKey] = expandedValue;
35
+ }
36
+ return result;
37
+ }
38
+ function setReqPropertySafe(target, key, value) {
39
+ try {
40
+ const desc = Object.getOwnPropertyDescriptor(target, key);
41
+ if (desc && desc.configurable === false && desc.writable === false) {
42
+ return;
43
+ }
44
+ if (!desc || desc.configurable !== false) {
45
+ Object.defineProperty(target, key, {
46
+ value,
47
+ writable: true,
48
+ configurable: true,
49
+ enumerable: true
50
+ });
51
+ return;
52
+ }
53
+ } catch (_) {
54
+ }
55
+ try {
56
+ target[key] = value;
57
+ } catch (_) {
58
+ }
59
+ }
60
+ function safeDeepClone(input) {
61
+ if (Array.isArray(input)) {
62
+ return input.map((v) => safeDeepClone(v));
63
+ }
64
+ if (isPlainObject(input)) {
65
+ const out = {};
66
+ for (const k of Object.keys(input)) {
67
+ if (!sanitizeKey(k)) continue;
68
+ out[k] = safeDeepClone(input[k]);
69
+ }
70
+ return out;
71
+ }
72
+ return input;
73
+ }
74
+ function mergeValues(values, strategy) {
75
+ switch (strategy) {
76
+ case "keepFirst":
77
+ return values[0];
78
+ case "keepLast":
79
+ return values[values.length - 1];
80
+ case "combine":
81
+ return values.reduce((acc, v) => {
82
+ if (Array.isArray(v)) acc.push(...v);
83
+ else acc.push(v);
84
+ return acc;
85
+ }, []);
86
+ default:
87
+ return values[values.length - 1];
88
+ }
89
+ }
90
+ function isUrlEncodedContentType(req) {
91
+ const ct = String(req?.headers?.["content-type"] || "").toLowerCase();
92
+ return ct.startsWith("application/x-www-form-urlencoded");
93
+ }
94
+ function shouldExcludePath(path, excludePaths) {
95
+ if (!path || excludePaths.length === 0) return false;
96
+ const currentPath = path;
97
+ for (const p of excludePaths) {
98
+ if (p.endsWith("*")) {
99
+ if (currentPath.startsWith(p.slice(0, -1))) return true;
100
+ } else if (currentPath === p) {
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ }
106
+ function normalizeWhitelist(whitelist) {
107
+ if (!whitelist) return [];
108
+ if (typeof whitelist === "string") return [whitelist];
109
+ return whitelist.filter((w) => typeof w === "string");
110
+ }
111
+ function buildWhitelistHelpers(whitelist) {
112
+ const exact = new Set(whitelist);
113
+ const prefixes = whitelist.filter((w) => w.length > 0);
114
+ return {
115
+ exact,
116
+ prefixes,
117
+ isWhitelistedPath(pathParts) {
118
+ if (pathParts.length === 0) return false;
119
+ const full = pathParts.join(".");
120
+ if (exact.has(full)) return true;
121
+ const leaf = pathParts[pathParts.length - 1];
122
+ if (exact.has(leaf)) return true;
123
+ for (const p of prefixes) {
124
+ if (full === p || full.startsWith(p + ".")) return true;
125
+ }
126
+ return false;
127
+ }
128
+ };
129
+ }
130
+ function setIn(target, path, value) {
131
+ if (path.length === 0) {
132
+ return;
133
+ }
134
+ let cur = target;
135
+ for (let i = 0; i < path.length - 1; i++) {
136
+ const k = path[i];
137
+ if (!isPlainObject(cur[k])) cur[k] = {};
138
+ cur = cur[k];
139
+ }
140
+ const lastKey = path[path.length - 1];
141
+ cur[lastKey] = value;
142
+ }
143
+ function moveWhitelistedFromPolluted(reqPart, polluted, isWhitelisted) {
144
+ function walk(node, path = [], parent) {
145
+ for (const k of Object.keys(node)) {
146
+ const v = node[k];
147
+ const curPath = [...path, k];
148
+ if (isPlainObject(v)) {
149
+ walk(v, curPath, node);
150
+ if (Object.keys(v).length === 0) {
151
+ delete node[k];
152
+ }
153
+ } else {
154
+ if (isWhitelisted(curPath)) {
155
+ const normalizedPath = curPath.flatMap(
156
+ (seg) => seg.includes(".") ? seg.split(".") : [seg]
157
+ );
158
+ setIn(reqPart, normalizedPath, v);
159
+ delete node[k];
160
+ }
161
+ }
162
+ }
163
+ }
164
+ walk(polluted);
165
+ }
166
+ function detectAndReduce(input, opts) {
167
+ let keyCount = 0;
168
+ const polluted = {};
169
+ const pollutedKeys = [];
170
+ function processNode(node, path = [], depth = 0) {
171
+ if (node === null || node === void 0) return opts.preserveNull ? node : node;
172
+ if (Array.isArray(node)) {
173
+ const mapped = node.map((v) => processNode(v, path, depth));
174
+ if (opts.mergeStrategy === "combine") {
175
+ return mergeValues(mapped, "combine");
176
+ }
177
+ setIn(polluted, path, safeDeepClone(node));
178
+ pollutedKeys.push(path.join("."));
179
+ const reduced = mergeValues(mapped, opts.mergeStrategy);
180
+ return reduced;
181
+ }
182
+ if (isPlainObject(node)) {
183
+ if (depth > opts.maxDepth)
184
+ throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);
185
+ const out = {};
186
+ for (const rawKey of Object.keys(node)) {
187
+ keyCount++;
188
+ if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {
189
+ throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);
190
+ }
191
+ const safeKey = sanitizeKey(rawKey);
192
+ if (!safeKey) continue;
193
+ const child = node[rawKey];
194
+ const childPath = path.concat([safeKey]);
195
+ let value = processNode(child, childPath, depth + 1);
196
+ if (typeof value === "string" && opts.trimValues) value = value.trim();
197
+ out[safeKey] = value;
198
+ }
199
+ return out;
200
+ }
201
+ return node;
202
+ }
203
+ const cloned = safeDeepClone(input);
204
+ const cleaned = processNode(cloned, [], 0);
205
+ return { cleaned, pollutedTree: polluted, pollutedKeys };
206
+ }
207
+ function sanitize(input, options = {}) {
208
+ const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;
209
+ const whitelist = normalizeWhitelist(options.whitelist);
210
+ const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);
211
+ const {
212
+ mergeStrategy = DEFAULT_STRATEGY,
213
+ maxDepth = 20,
214
+ maxKeys = 5e3,
215
+ trimValues = false,
216
+ preserveNull = true
217
+ } = options;
218
+ const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {
219
+ mergeStrategy,
220
+ maxDepth,
221
+ maxKeys,
222
+ trimValues,
223
+ preserveNull
224
+ });
225
+ moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);
226
+ return cleaned;
227
+ }
228
+ function hppx(options = {}) {
229
+ const {
230
+ whitelist = [],
231
+ mergeStrategy = DEFAULT_STRATEGY,
232
+ sources = DEFAULT_SOURCES,
233
+ checkBodyContentType = "urlencoded",
234
+ excludePaths = [],
235
+ maxDepth = 20,
236
+ maxKeys = 5e3,
237
+ trimValues = false,
238
+ preserveNull = true,
239
+ strict = false,
240
+ onPollutionDetected,
241
+ logger
242
+ } = options;
243
+ const whitelistArr = normalizeWhitelist(whitelist);
244
+ const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);
245
+ return function hppxMiddleware(req, res, next) {
246
+ try {
247
+ if (shouldExcludePath(req?.path, excludePaths)) return next();
248
+ let anyPollutionDetected = false;
249
+ const allPollutedKeys = [];
250
+ for (const source of sources) {
251
+ if (!req || typeof req !== "object") break;
252
+ if (req[source] === void 0) continue;
253
+ if (source === "body") {
254
+ if (checkBodyContentType === "none") continue;
255
+ if (checkBodyContentType === "urlencoded" && !isUrlEncodedContentType(req)) continue;
256
+ }
257
+ const part = req[source];
258
+ if (!isPlainObject(part)) continue;
259
+ const expandedPart = expandObjectPaths(part);
260
+ const pollutedKey = `${source}Polluted`;
261
+ const processedKey = `__hppxProcessed_${source}`;
262
+ const hasProcessedBefore = Boolean(req[processedKey]);
263
+ if (!hasProcessedBefore) {
264
+ const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {
265
+ mergeStrategy,
266
+ maxDepth,
267
+ maxKeys,
268
+ trimValues,
269
+ preserveNull
270
+ });
271
+ setReqPropertySafe(req, source, cleaned);
272
+ setReqPropertySafe(req, pollutedKey, pollutedTree);
273
+ req[processedKey] = true;
274
+ moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
275
+ if (pollutedKeys.length > 0) {
276
+ anyPollutionDetected = true;
277
+ for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);
278
+ }
279
+ } else {
280
+ moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);
281
+ }
282
+ }
283
+ if (anyPollutionDetected) {
284
+ if (onPollutionDetected) {
285
+ try {
286
+ onPollutionDetected(req, {
287
+ source: "query",
288
+ pollutedKeys: allPollutedKeys
289
+ });
290
+ } catch (_) {
291
+ }
292
+ }
293
+ if (strict && res && typeof res.status === "function") {
294
+ return res.status(400).json({
295
+ error: "Bad Request",
296
+ message: "HTTP Parameter Pollution detected",
297
+ pollutedParameters: allPollutedKeys,
298
+ code: "HPP_DETECTED"
299
+ });
300
+ }
301
+ }
302
+ return next();
303
+ } catch (err) {
304
+ if (logger) {
305
+ try {
306
+ logger(err);
307
+ } catch (_) {
308
+ }
309
+ }
310
+ return next(err);
311
+ }
312
+ };
313
+ }
314
+ export {
315
+ DANGEROUS_KEYS,
316
+ DEFAULT_SOURCES,
317
+ DEFAULT_STRATEGY,
318
+ hppx as default,
319
+ sanitize
320
+ };
321
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\r\n * hppx — Superior HTTP Parameter Pollution protection middleware\r\n *\r\n * - Protects against parameter and prototype pollution\r\n * - Supports nested whitelists via dot-notation and leaf matching\r\n * - Merge strategies: keepFirst | keepLast | combine\r\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\r\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\r\n * - TypeScript-first API\r\n */\r\n\r\nexport type RequestSource = \"query\" | \"body\" | \"params\";\r\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\r\n\r\nexport interface SanitizeOptions {\r\n whitelist?: string[] | string;\r\n mergeStrategy?: MergeStrategy;\r\n maxDepth?: number;\r\n maxKeys?: number;\r\n trimValues?: boolean;\r\n preserveNull?: boolean;\r\n}\r\n\r\nexport interface HppxOptions extends SanitizeOptions {\r\n sources?: RequestSource[];\r\n /** When to process req.body */\r\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\r\n excludePaths?: string[];\r\n strict?: boolean;\r\n onPollutionDetected?: (req: any, info: { source: RequestSource; pollutedKeys: string[] }) => void;\r\n logger?: (err: unknown) => void;\r\n}\r\n\r\nexport interface SanitizedResult<T> {\r\n cleaned: T;\r\n pollutedTree: Record<string, unknown>;\r\n pollutedKeys: string[];\r\n}\r\n\r\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\r\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\r\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== \"object\") return false;\r\n const proto = Object.getPrototypeOf(value);\r\n return proto === Object.prototype || proto === null;\r\n}\r\n\r\nfunction sanitizeKey(key: string): string | null {\r\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\r\n if (DANGEROUS_KEYS.has(key)) return null;\r\n if (key.includes(\"\\u0000\")) return null;\r\n return key;\r\n}\r\n\r\nfunction parsePathSegments(key: string): string[] {\r\n // Convert bracket notation to dots, then split\r\n // a[b][c] -> a.b.c\r\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\r\n return dotted.split(\".\").filter((s) => s.length > 0);\r\n}\r\n\r\nfunction expandObjectPaths(obj: Record<string, unknown>): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(obj)) {\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const value = (obj as any)[rawKey];\r\n\r\n // Recursively expand nested objects first\r\n const expandedValue = isPlainObject(value)\r\n ? expandObjectPaths(value as Record<string, unknown>)\r\n : value;\r\n\r\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\r\n const segments = parsePathSegments(safeKey);\r\n if (segments.length > 0) {\r\n setIn(result, segments, expandedValue);\r\n continue;\r\n }\r\n }\r\n result[safeKey] = expandedValue;\r\n }\r\n return result;\r\n}\r\n\r\nfunction setReqPropertySafe(target: any, key: string, value: unknown): void {\r\n try {\r\n const desc = Object.getOwnPropertyDescriptor(target, key);\r\n if (desc && desc.configurable === false && desc.writable === false) {\r\n // Non-configurable and not writable: skip\r\n return;\r\n }\r\n if (!desc || desc.configurable !== false) {\r\n Object.defineProperty(target, key, {\r\n value,\r\n writable: true,\r\n configurable: true,\r\n enumerable: true,\r\n });\r\n return;\r\n }\r\n } catch (_) {\r\n // fall back to assignment below\r\n }\r\n try {\r\n target[key] = value;\r\n } catch (_) {\r\n // last resort: skip if cannot assign\r\n }\r\n}\r\n\r\nfunction safeDeepClone<T>(input: T): T {\r\n if (Array.isArray(input)) {\r\n return input.map((v) => safeDeepClone(v)) as T;\r\n }\r\n if (isPlainObject(input)) {\r\n const out: Record<string, unknown> = {};\r\n for (const k of Object.keys(input)) {\r\n if (!sanitizeKey(k)) continue;\r\n out[k] = safeDeepClone((input as Record<string, unknown>)[k]);\r\n }\r\n return out as T;\r\n }\r\n return input;\r\n}\r\n\r\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\r\n switch (strategy) {\r\n case \"keepFirst\":\r\n return values[0];\r\n case \"keepLast\":\r\n return values[values.length - 1];\r\n case \"combine\":\r\n return values.reduce<unknown[]>((acc, v) => {\r\n if (Array.isArray(v)) acc.push(...v);\r\n else acc.push(v);\r\n return acc;\r\n }, []);\r\n default:\r\n return values[values.length - 1];\r\n }\r\n}\r\n\r\nfunction isUrlEncodedContentType(req: any): boolean {\r\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\r\n return ct.startsWith(\"application/x-www-form-urlencoded\");\r\n}\r\n\r\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\r\n if (!path || excludePaths.length === 0) return false;\r\n const currentPath = path;\r\n for (const p of excludePaths) {\r\n if (p.endsWith(\"*\")) {\r\n if (currentPath.startsWith(p.slice(0, -1))) return true;\r\n } else if (currentPath === p) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n}\r\n\r\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\r\n if (!whitelist) return [];\r\n if (typeof whitelist === \"string\") return [whitelist];\r\n return whitelist.filter((w) => typeof w === \"string\");\r\n}\r\n\r\nfunction buildWhitelistHelpers(whitelist: string[]) {\r\n const exact = new Set(whitelist);\r\n const prefixes = whitelist.filter((w) => w.length > 0);\r\n return {\r\n exact,\r\n prefixes,\r\n isWhitelistedPath(pathParts: string[]): boolean {\r\n if (pathParts.length === 0) return false;\r\n const full = pathParts.join(\".\");\r\n if (exact.has(full)) return true;\r\n // leaf match\r\n const leaf = pathParts[pathParts.length - 1]!;\r\n if (exact.has(leaf)) return true;\r\n // prefix match (treat any listed segment as prefix of a subtree)\r\n for (const p of prefixes) {\r\n if (full === p || full.startsWith(p + \".\")) return true;\r\n }\r\n return false;\r\n },\r\n };\r\n}\r\n\r\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\r\n /* istanbul ignore if */\r\n if (path.length === 0) {\r\n return;\r\n }\r\n let cur: any = target;\r\n for (let i = 0; i < path.length - 1; i++) {\r\n const k = path[i]!;\r\n if (!isPlainObject(cur[k])) cur[k] = {};\r\n cur = cur[k];\r\n }\r\n const lastKey = path[path.length - 1]!;\r\n cur[lastKey] = value;\r\n}\r\n\r\nfunction moveWhitelistedFromPolluted(\r\n reqPart: Record<string, unknown>,\r\n polluted: Record<string, unknown>,\r\n isWhitelisted: (path: string[]) => boolean,\r\n): void {\r\n function walk(\r\n node: Record<string, unknown>,\r\n path: string[] = [],\r\n parent?: Record<string, unknown>,\r\n ) {\r\n for (const k of Object.keys(node)) {\r\n const v = node[k];\r\n const curPath = [...path, k];\r\n if (isPlainObject(v)) {\r\n walk(v as Record<string, unknown>, curPath, node);\r\n // prune empty objects\r\n if (Object.keys(v as Record<string, unknown>).length === 0) {\r\n delete (node as any)[k];\r\n }\r\n } else {\r\n if (isWhitelisted(curPath)) {\r\n // put back into request\r\n const normalizedPath = curPath.flatMap((seg) =>\r\n seg.includes(\".\") ? seg.split(\".\") : [seg],\r\n );\r\n setIn(reqPart, normalizedPath, v);\r\n delete (node as any)[k];\r\n }\r\n }\r\n }\r\n }\r\n walk(polluted);\r\n}\r\n\r\nfunction detectAndReduce(\r\n input: Record<string, unknown>,\r\n opts: Required<\r\n Pick<SanitizeOptions, \"mergeStrategy\" | \"maxDepth\" | \"maxKeys\" | \"trimValues\" | \"preserveNull\">\r\n >,\r\n): SanitizedResult<Record<string, unknown>> {\r\n let keyCount = 0;\r\n const polluted: Record<string, unknown> = {};\r\n const pollutedKeys: string[] = [];\r\n\r\n function processNode(node: unknown, path: string[] = [], depth = 0): unknown {\r\n if (node === null || node === undefined) return opts.preserveNull ? node : node;\r\n\r\n if (Array.isArray(node)) {\r\n const mapped = node.map((v) => processNode(v, path, depth));\r\n if (opts.mergeStrategy === \"combine\") {\r\n // combine: do not record pollution, but flatten using mergeValues\r\n return mergeValues(mapped, \"combine\");\r\n }\r\n // Other strategies: record pollution and reduce\r\n setIn(polluted, path, safeDeepClone(node));\r\n pollutedKeys.push(path.join(\".\"));\r\n const reduced = mergeValues(mapped, opts.mergeStrategy);\r\n return reduced;\r\n }\r\n\r\n if (isPlainObject(node)) {\r\n if (depth > opts.maxDepth)\r\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\r\n const out: Record<string, unknown> = {};\r\n for (const rawKey of Object.keys(node)) {\r\n keyCount++;\r\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\r\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\r\n }\r\n const safeKey = sanitizeKey(rawKey);\r\n if (!safeKey) continue;\r\n const child = (node as Record<string, unknown>)[rawKey];\r\n const childPath = path.concat([safeKey]);\r\n let value = processNode(child, childPath, depth + 1);\r\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\r\n out[safeKey] = value;\r\n }\r\n return out;\r\n }\r\n\r\n return node;\r\n }\r\n\r\n const cloned = safeDeepClone(input);\r\n const cleaned = processNode(cloned, [], 0) as Record<string, unknown>;\r\n return { cleaned, pollutedTree: polluted, pollutedKeys };\r\n}\r\n\r\nexport function sanitize<T extends Record<string, unknown>>(\r\n input: T,\r\n options: SanitizeOptions = {},\r\n): T {\r\n // Normalize and expand keys prior to sanitization\r\n const expandedInput = isPlainObject(input) ? expandObjectPaths(input) : input;\r\n const whitelist = normalizeWhitelist(options.whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\r\n const {\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n } = options;\r\n\r\n // First: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n // Second: move back whitelisted arrays\r\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\r\n\r\n return cleaned as T;\r\n}\r\n\r\ntype ExpressLikeNext = (err?: any) => void;\r\n\r\nexport default function hppx(options: HppxOptions = {}) {\r\n const {\r\n whitelist = [],\r\n mergeStrategy = DEFAULT_STRATEGY,\r\n sources = DEFAULT_SOURCES,\r\n checkBodyContentType = \"urlencoded\",\r\n excludePaths = [],\r\n maxDepth = 20,\r\n maxKeys = 5000,\r\n trimValues = false,\r\n preserveNull = true,\r\n strict = false,\r\n onPollutionDetected,\r\n logger,\r\n } = options;\r\n\r\n const whitelistArr = normalizeWhitelist(whitelist);\r\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\r\n\r\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\r\n try {\r\n if (shouldExcludePath(req?.path, excludePaths)) return next();\r\n\r\n let anyPollutionDetected = false;\r\n const allPollutedKeys: string[] = [];\r\n\r\n for (const source of sources) {\r\n /* istanbul ignore next */ if (!req || typeof req !== \"object\") break;\r\n if (req[source] === undefined) continue;\r\n\r\n if (source === \"body\") {\r\n if (checkBodyContentType === \"none\") continue;\r\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\r\n }\r\n\r\n const part = req[source];\r\n if (!isPlainObject(part)) continue;\r\n\r\n // Preprocess: expand dotted and bracketed keys into nested objects\r\n const expandedPart = expandObjectPaths(part);\r\n\r\n const pollutedKey = `${source}Polluted`;\r\n const processedKey = `__hppxProcessed_${source}`;\r\n const hasProcessedBefore = Boolean((req as any)[processedKey]);\r\n\r\n if (!hasProcessedBefore) {\r\n // First pass for this request part: reduce arrays and collect polluted\r\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\r\n mergeStrategy,\r\n maxDepth,\r\n maxKeys,\r\n trimValues,\r\n preserveNull,\r\n });\r\n\r\n setReqPropertySafe(req, source, cleaned);\r\n\r\n // Attach polluted object (always present as {} when source processed)\r\n setReqPropertySafe(req, pollutedKey, pollutedTree);\r\n (req as any)[processedKey] = true;\r\n\r\n // Apply whitelist now: move whitelisted arrays back\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n\r\n if (pollutedKeys.length > 0) {\r\n anyPollutionDetected = true;\r\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\r\n }\r\n } else {\r\n // Subsequent middleware: only put back whitelisted entries\r\n moveWhitelistedFromPolluted(req[source], req[pollutedKey], isWhitelistedPath);\r\n // pollution already accounted for in previous pass\r\n }\r\n }\r\n\r\n if (anyPollutionDetected) {\r\n if (onPollutionDetected) {\r\n try {\r\n onPollutionDetected(req, {\r\n source: \"query\",\r\n pollutedKeys: allPollutedKeys,\r\n });\r\n } catch (_) {\r\n /* ignore user callback errors */\r\n }\r\n }\r\n if (strict && res && typeof res.status === \"function\") {\r\n return res.status(400).json({\r\n error: \"Bad Request\",\r\n message: \"HTTP Parameter Pollution detected\",\r\n pollutedParameters: allPollutedKeys,\r\n code: \"HPP_DETECTED\",\r\n });\r\n }\r\n }\r\n\r\n return next();\r\n } catch (err) {\r\n if (logger) {\r\n try {\r\n logger(err);\r\n } catch (_) {\r\n /* noop */\r\n }\r\n }\r\n return next(err);\r\n }\r\n };\r\n}\r\n\r\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "hppx",
3
+ "version": "0.1.0",
4
+ "description": "Superior HTTP Parameter Pollution protection middleware with modern TypeScript, robust sanitizer, and extensive tests.",
5
+ "license": "MIT",
6
+ "author": "Hiprax",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Hiprax/hppx"
10
+ },
11
+ "homepage": "https://github.com/Hiprax/hppx#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Hiprax/hppx/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "dist/index.cjs",
17
+ "module": "dist/index.mjs",
18
+ "types": "dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "sideEffects": false,
32
+ "engines": {
33
+ "node": ">=16"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "clean": "rimraf dist",
41
+ "dev": "tsup --watch",
42
+ "test": "jest",
43
+ "test:coverage": "jest --coverage",
44
+ "test:watch": "jest --watch",
45
+ "prepare": "npm run build",
46
+ "typecheck": "tsc --noEmit",
47
+ "lint": "eslint \"{src,tests}/**/*.{ts,tsx}\"",
48
+ "format": "prettier --write \"**/*.{ts,tsx,js,json,md,yml,yaml}\""
49
+ },
50
+ "devDependencies": {
51
+ "@types/express": "^5.0.5",
52
+ "@types/jest": "^30.0.0",
53
+ "@types/node": "^24.10.0",
54
+ "@types/supertest": "^6.0.3",
55
+ "@typescript-eslint/eslint-plugin": "^8.46.3",
56
+ "@typescript-eslint/parser": "^8.46.3",
57
+ "eslint": "^9.39.1",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "express": "^5.1.0",
60
+ "jest": "^30.2.0",
61
+ "patch-package": "^8.0.1",
62
+ "prettier": "^3.6.2",
63
+ "rimraf": "^6.1.0",
64
+ "supertest": "^7.1.4",
65
+ "ts-jest": "^29.4.5",
66
+ "ts-node": "^10.9.2",
67
+ "tsup": "^8.5.0",
68
+ "typescript": "^5.9.3"
69
+ }
70
+ }