hppx 0.1.10 → 0.2.3

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.
@@ -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 /* 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":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * hppx — Superior HTTP Parameter Pollution protection middleware\n *\n * - Protects against parameter and prototype pollution\n * - Supports nested whitelists via dot-notation and leaf matching\n * - Merge strategies: keepFirst | keepLast | combine\n * - Multiple middleware compatibility: arrays are \"put aside\" once and selectively restored\n * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted\n * - TypeScript-first API\n */\n\n// Augment the Express Request interface with hppx-specific properties so that\n// consumers importing this package automatically get typed access to the\n// polluted-tree fields. The `declare module` block lives here (rather than in\n// a separate `.d.ts` file) so that tsup emits it into both `dist/index.d.ts`\n// and `dist/index.d.cts`, which are kept in symbol-parity by\n// `scripts/check-dts-parity.mjs`.\n//\n// The `import type` below is required for TypeScript 6.0+ DTS rollup, which no\n// longer resolves augmentation targets through transitive `@types` packages.\n// It pulls the augmentation module into scope without affecting the JS output.\nimport type {} from \"express-serve-static-core\";\n\ndeclare module \"express-serve-static-core\" {\n interface Request {\n queryPolluted?: Record<string, unknown>;\n bodyPolluted?: Record<string, unknown>;\n paramsPolluted?: Record<string, unknown>;\n }\n}\n\nexport type RequestSource = \"query\" | \"body\" | \"params\";\nexport type MergeStrategy = \"keepFirst\" | \"keepLast\" | \"combine\";\n\nexport interface SanitizeOptions {\n whitelist?: string[] | string;\n mergeStrategy?: MergeStrategy;\n maxDepth?: number;\n maxKeys?: number;\n maxArrayLength?: number;\n maxKeyLength?: number;\n trimValues?: boolean;\n preserveNull?: boolean;\n}\n\nexport interface HppxOptions extends SanitizeOptions {\n sources?: RequestSource[];\n /** When to process req.body */\n checkBodyContentType?: \"urlencoded\" | \"any\" | \"none\";\n excludePaths?: string[];\n strict?: boolean;\n onPollutionDetected?: (\n req: Record<string, unknown>,\n info: { source: RequestSource; pollutedKeys: string[] },\n ) => void;\n logger?: (err: Error | unknown) => void;\n /** Enable logging when pollution is detected (default: true) */\n logPollution?: boolean;\n}\n\nexport interface SanitizedResult<T> {\n cleaned: T;\n pollutedTree: Record<string, unknown>;\n pollutedKeys: string[];\n}\n\nconst DEFAULT_SOURCES: RequestSource[] = [\"query\", \"body\", \"params\"];\nconst DEFAULT_STRATEGY: MergeStrategy = \"keepLast\";\nconst DANGEROUS_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\n// Pre-compiled, ReDoS-safe character class that rejects keys containing any of:\n// - ASCII C0 controls (U+0000..U+001F) and DEL (U+007F)\n// - C1 controls (U+0080..U+009F)\n// - Unicode bidirectional control characters: U+200E/U+200F (LRM/RLM),\n// U+202A..U+202E (LRE/RLE/PDF/LRO/RLO), U+2066..U+2069 (LRI/RLI/FSI/PDI)\n// - U+FEFF (BOM / zero-width no-break space) — typically accidental in keys\n// and used to bypass naive key-based logic.\n// Rejecting these prevents bypass of downstream key-based logic, log injection,\n// and DB/log corruption when output is not properly quoted. The character\n// class contains no quantifiers, so it cannot trigger ReDoS.\n/* eslint-disable no-control-regex -- intentional: this regex must include control characters in order to reject them */\nconst FORBIDDEN_KEY_CHARS =\n /[\\u0000-\\u001F\\u007F-\\u009F\\u200E\\u200F\\u202A-\\u202E\\u2066-\\u2069\\uFEFF]/;\n/* eslint-enable no-control-regex */\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction sanitizeKey(key: string, maxKeyLength?: number): string | null {\n /* istanbul ignore next */ if (typeof key !== \"string\") return null;\n if (DANGEROUS_KEYS.has(key)) return null;\n // Reject keys containing ASCII/Unicode control characters or bidirectional\n // override characters. Subsumes the previous explicit NUL-byte check.\n if (FORBIDDEN_KEY_CHARS.test(key)) return null;\n // Prevent excessively long keys that could cause DoS\n /* istanbul ignore next -- defensive: callers always pass maxKeyLength explicitly */\n const maxLen = maxKeyLength ?? 200;\n if (key.length > maxLen) return null;\n // Prevent keys that are only dots or brackets (malformed) - but allow single dot as it's valid\n if (key.length > 1 && /^[.\\[\\]]+$/.test(key)) return null;\n return key;\n}\n\n// Cache for parsed path segments to improve performance.\n// Uses a clear-on-full eviction policy: when the cache reaches capacity, it is\n// cleared in its entirety so future entries can still be cached. This prevents\n// a one-shot poisoning attack where an attacker pumps unique keys into a single\n// request to permanently disable the cache for legitimate traffic.\nconst PATH_SEGMENT_CACHE_LIMIT = 500;\nconst pathSegmentCache = new Map<string, string[]>();\n\n/**\n * Parse a (possibly composite) key into an array of path segments.\n *\n * Accepted forms (and how they're parsed):\n * - Dotted: \"a.b.c\" -> [\"a\", \"b\", \"c\"]\n * - Bracket: \"a[b][c]\" -> [\"a\", \"b\", \"c\"]\n * - Mixed: \"a.b[c].d\" -> [\"a\", \"b\", \"c\", \"d\"]\n * - Numeric brackets: \"a[0][1]\" -> [\"a\", \"0\", \"1\"] (indexes become string segments)\n * - Trailing/empty: \"a..b\" / \"a[]\" -> [\"a\", \"b\"] / [\"a\"] (empty segments dropped)\n *\n * **The parser is intentionally lenient.** Malformed inputs like `a]]b[[c`\n * collapse to `[\"a\", \"b\", \"c\"]` rather than being rejected. Justification:\n *\n * 1. Every key reaching this function has already passed `sanitizeKey`,\n * which rejects truly hostile inputs — dangerous keys (`__proto__`,\n * `prototype`, `constructor`), control characters, bidirectional\n * override characters, dot/bracket-only patterns, and overly long\n * strings. The remaining surface is benign syntactic noise.\n *\n * 2. Lenient parsing is acceptable defense-in-depth: even if an attacker\n * crafts unusual bracket placement, the parsed segments are still\n * passed through `sanitizeKey` again at every level by `setIn`, which\n * blocks dangerous keys at the segment level too.\n *\n * 3. Strict grammar enforcement here would be a behavioral change that\n * could break legitimate users with unusual key shapes, while\n * providing little additional security beyond what `sanitizeKey`\n * already enforces.\n *\n * The regexes are character-class only (no quantifiers), so this function is\n * ReDoS-safe.\n */\nfunction parsePathSegments(key: string): string[] {\n // Check cache first\n const cached = pathSegmentCache.get(key);\n if (cached) return cached;\n\n // Convert bracket notation to dots, then split\n // a[b][c] -> a.b.c\n const dotted = key.replace(/\\]/g, \"\").replace(/\\[/g, \".\");\n const result = dotted.split(\".\").filter((s) => s.length > 0);\n\n // Clear-on-full eviction: prevents permanent cache disablement after a flood\n // of unique keys, while keeping the implementation O(1) on insert.\n if (pathSegmentCache.size >= PATH_SEGMENT_CACHE_LIMIT) {\n pathSegmentCache.clear();\n }\n pathSegmentCache.set(key, result);\n\n return result;\n}\n\n/**\n * @internal — test-only helper that resets the module-level path segment cache.\n * Public callers must not depend on this; it exists solely so tests can verify\n * cache eviction behavior without exposing the internal Map.\n */\nexport function __resetPathSegmentCache(): void {\n pathSegmentCache.clear();\n}\n\nfunction expandObjectPaths(\n obj: Record<string, unknown>,\n maxKeyLength?: number,\n maxDepth = 20,\n currentDepth = 0,\n seen?: WeakSet<object>,\n): Record<string, unknown> {\n if (currentDepth > maxDepth) {\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\n }\n // Path-stack cycle detection: only detect references currently on the recursion\n // stack (true cycles), not previously-visited-but-fully-emitted nodes (shared\n // acyclic subtrees, which must be fully cloned at each occurrence).\n const seenSet = seen ?? new WeakSet<object>();\n if (seenSet.has(obj)) return {};\n seenSet.add(obj);\n try {\n const result: Record<string, unknown> = {};\n for (const rawKey of Object.keys(obj)) {\n const safeKey = sanitizeKey(rawKey, maxKeyLength);\n if (!safeKey) continue;\n const value = obj[rawKey];\n\n // Recursively expand nested objects first\n const expandedValue = isPlainObject(value)\n ? expandObjectPaths(\n value as Record<string, unknown>,\n maxKeyLength,\n maxDepth,\n currentDepth + 1,\n seenSet,\n )\n : value;\n\n if (safeKey.includes(\".\") || safeKey.includes(\"[\")) {\n const segments = parsePathSegments(safeKey);\n if (segments.length > 0) {\n setIn(result, segments, expandedValue);\n continue;\n }\n }\n result[safeKey] = expandedValue;\n }\n return result;\n } finally {\n seenSet.delete(obj);\n }\n}\n\n/**\n * Attempts to set a property on a request-like object.\n *\n * Returns `true` if the property is observable as the requested value after the call,\n * `false` if every attempt failed.\n *\n * Strategy:\n * 1. If no own descriptor exists, or the descriptor is configurable, redefine via\n * `Object.defineProperty` so the assigned value shadows any prototype-level getter\n * (e.g. Express 5's lazy `req.query` getter on `IncomingMessage.prototype`).\n * 2. If the descriptor is non-configurable BUT writable, fall through to direct\n * assignment.\n * 3. If the descriptor is non-configurable AND non-writable, attempt direct assignment\n * in a `try/catch` (in strict mode it will throw); on failure, surface a warning via\n * the supplied logger so the user knows the request continues to expose the original\n * (potentially polluted) value.\n */\nfunction setReqPropertySafe(\n target: Record<string, unknown>,\n key: string,\n value: unknown,\n onFailure?: (message: string) => void,\n): boolean {\n try {\n const desc = Object.getOwnPropertyDescriptor(target, key);\n if (!desc || desc.configurable !== false) {\n Object.defineProperty(target, key, {\n value,\n writable: true,\n configurable: true,\n enumerable: true,\n });\n return true;\n }\n // desc is non-configurable; defineProperty cannot be used.\n if (desc.writable) {\n target[key] = value;\n return true;\n }\n // Non-configurable + non-writable: attempt direct assignment which will throw in\n // strict mode. Do NOT silently skip — surface the failure.\n try {\n target[key] = value;\n /* istanbul ignore next -- sloppy-mode read-back unreachable here:\n this module is ESM (implicitly strict mode), so the assignment above\n throws on any non-writable target. The read-back guards against the\n sloppy-mode case where the assignment silently no-ops. */\n if (target[key] === value) return true;\n } catch (_assignErr) {\n // fall through to the warning below\n }\n if (onFailure) {\n onFailure(\n `[hppx] Could not write sanitized value to req.${key}: property is non-configurable and non-writable. The original (potentially polluted) value remains on req.${key}.`,\n );\n }\n return false;\n } catch (_definePropErr) {\n // defineProperty itself threw — try plain assignment as a last resort.\n try {\n target[key] = value;\n if (target[key] === value) return true;\n } catch (_assignErr) {\n // fall through\n }\n /* istanbul ignore next -- defensive fallback for an extreme edge case:\n reaching here requires defineProperty to throw AND the subsequent\n direct assignment to either throw or silently no-op AND an onFailure\n callback to be configured. Exercised indirectly via the assignment\n fallback test (which patches defineProperty to throw). */\n {\n if (onFailure) {\n onFailure(`[hppx] Could not write sanitized value to req.${key}: defineProperty failed.`);\n }\n return false;\n }\n }\n}\n\nfunction safeDeepClone<T>(\n input: T,\n maxKeyLength?: number,\n maxArrayLength?: number,\n maxDepth = 20,\n currentDepth = 0,\n seen?: WeakSet<object>,\n): T {\n // Path-stack cycle detection: a node is added to `seen` on entry and removed\n // on exit (via `finally`). This correctly distinguishes true cycles\n // (currently on the recursion stack) from shared acyclic subtrees (already\n // emitted but no longer on the stack — must be cloned independently).\n if (Array.isArray(input)) {\n if (currentDepth > maxDepth) {\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\n }\n const seenSet = seen ?? new WeakSet<object>();\n if (seenSet.has(input)) return [] as T;\n seenSet.add(input);\n try {\n // Limit array length to prevent memory exhaustion\n const limit = maxArrayLength ?? 1000;\n const limited = input.slice(0, limit);\n return limited.map((v) =>\n safeDeepClone(v, maxKeyLength, maxArrayLength, maxDepth, currentDepth + 1, seenSet),\n ) as T;\n } finally {\n seenSet.delete(input);\n }\n }\n if (isPlainObject(input)) {\n if (currentDepth > maxDepth) {\n throw new Error(`Maximum object depth (${maxDepth}) exceeded`);\n }\n const seenSet = seen ?? new WeakSet<object>();\n if (seenSet.has(input as object)) return {} as T;\n seenSet.add(input as object);\n try {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(input)) {\n if (!sanitizeKey(k, maxKeyLength)) continue;\n out[k] = safeDeepClone(\n (input as Record<string, unknown>)[k],\n maxKeyLength,\n maxArrayLength,\n maxDepth,\n currentDepth + 1,\n seenSet,\n );\n }\n return out as T;\n } finally {\n seenSet.delete(input as object);\n }\n }\n return input;\n}\n\nfunction mergeValues(values: unknown[], strategy: MergeStrategy): unknown {\n switch (strategy) {\n case \"keepFirst\":\n return values[0];\n case \"keepLast\":\n return values[values.length - 1];\n case \"combine\":\n return values.reduce<unknown[]>((acc, v) => {\n if (Array.isArray(v)) acc.push(...v);\n else acc.push(v);\n return acc;\n }, []);\n /* istanbul ignore next -- exhaustiveness check unreachable from outside:\n validateSanitizeOptions rejects every non-listed strategy at construction\n time, so the only way to reach this branch is a programmer error (a new\n MergeStrategy union member added without updating this switch). Failing\n loudly here is preferable to a silent fallback. */\n default: {\n const _exhaustive: never = strategy;\n throw new Error(`Unknown mergeStrategy: ${_exhaustive as string}`);\n }\n }\n}\n\nfunction isUrlEncodedContentType(req: any): boolean {\n const ct = String(req?.headers?.[\"content-type\"] || \"\").toLowerCase();\n return ct.startsWith(\"application/x-www-form-urlencoded\");\n}\n\nfunction shouldExcludePath(path: string | undefined, excludePaths: string[]): boolean {\n if (!path || excludePaths.length === 0) return false;\n const currentPath = path;\n for (const p of excludePaths) {\n if (p.endsWith(\"*\")) {\n if (currentPath.startsWith(p.slice(0, -1))) return true;\n } else if (currentPath === p) {\n return true;\n }\n }\n return false;\n}\n\nfunction normalizeWhitelist(whitelist?: string[] | string): string[] {\n if (!whitelist) return [];\n if (typeof whitelist === \"string\") return [whitelist];\n return whitelist.filter((w) => typeof w === \"string\");\n}\n\nconst WHITELIST_PATH_CACHE_LIMIT = 1000;\n\nfunction buildWhitelistHelpers(whitelist: string[]) {\n const exact = new Set(whitelist);\n const prefixes = whitelist.filter((w) => w.length > 0);\n // Pre-build a cache for commonly checked paths for performance. Uses a\n // clear-on-full eviction policy (matching `pathSegmentCache`) to prevent a\n // poisoning attack where an attacker pumps unique paths to permanently\n // disable caching for legitimate paths.\n const pathCache = new Map<string, boolean>();\n\n return {\n exact,\n prefixes,\n isWhitelistedPath(pathParts: string[]): boolean {\n /* istanbul ignore if -- defensive: always called with non-empty path from walk() */\n if (pathParts.length === 0) return false;\n const full = pathParts.join(\".\");\n\n // Check cache first for performance\n const cached = pathCache.get(full);\n if (cached !== undefined) return cached;\n\n let result = false;\n\n // Exact match\n if (exact.has(full)) {\n result = true;\n }\n // Leaf match\n else if (exact.has(pathParts[pathParts.length - 1]!)) {\n result = true;\n }\n // Prefix match (treat any listed segment as prefix of a subtree)\n else {\n for (const p of prefixes) {\n if (full === p || full.startsWith(p + \".\")) {\n result = true;\n break;\n }\n }\n }\n\n // Clear-on-full eviction: prevents permanent cache disablement after a\n // flood of unique paths, while keeping the implementation O(1) on insert.\n if (pathCache.size >= WHITELIST_PATH_CACHE_LIMIT) {\n pathCache.clear();\n }\n pathCache.set(full, result);\n\n return result;\n },\n };\n}\n\nfunction setIn(target: Record<string, unknown>, path: string[], value: unknown): void {\n /* istanbul ignore if */\n if (path.length === 0) {\n return;\n }\n let cur: Record<string, unknown> = target;\n for (let i = 0; i < path.length - 1; i++) {\n const k = path[i]!;\n // Additional prototype pollution protection\n if (DANGEROUS_KEYS.has(k)) return;\n if (!isPlainObject(cur[k])) {\n // Create a new plain object to avoid pollution\n cur[k] = {};\n }\n cur = cur[k] as Record<string, unknown>;\n }\n const lastKey = path[path.length - 1]!;\n // Final check on the last key\n if (DANGEROUS_KEYS.has(lastKey)) return;\n cur[lastKey] = value;\n}\n\nfunction moveWhitelistedFromPolluted(\n reqPart: Record<string, unknown>,\n polluted: Record<string, unknown>,\n isWhitelisted: (path: string[]) => boolean,\n): void {\n function walk(node: Record<string, unknown>, path: string[] = []) {\n for (const k of Object.keys(node)) {\n const v = node[k];\n const curPath = [...path, k];\n if (isPlainObject(v)) {\n walk(v as Record<string, unknown>, curPath);\n // prune empty objects\n if (Object.keys(v as Record<string, unknown>).length === 0) {\n delete node[k];\n }\n } else {\n if (isWhitelisted(curPath)) {\n // put back into request\n /* istanbul ignore next -- defensive: polluted tree keys never contain dots after expansion */\n const normalizedPath = curPath.flatMap((seg) =>\n seg.includes(\".\") ? seg.split(\".\") : [seg],\n );\n setIn(reqPart, normalizedPath, v);\n delete node[k];\n }\n }\n }\n }\n walk(polluted);\n}\n\nfunction detectAndReduce(\n input: Record<string, unknown>,\n opts: Required<\n Pick<\n SanitizeOptions,\n | \"mergeStrategy\"\n | \"maxDepth\"\n | \"maxKeys\"\n | \"maxArrayLength\"\n | \"maxKeyLength\"\n | \"trimValues\"\n | \"preserveNull\"\n >\n >,\n): SanitizedResult<Record<string, unknown>> {\n let keyCount = 0;\n const polluted: Record<string, unknown> = {};\n // Use a Set for de-duplication. Multiple arrays can land at the same dotted\n // path (e.g. `[{tags:[...]}, {tags:[...]}]` produces two array sites at\n // `items.tags`); the user-facing `pollutedKeys` list should report each\n // affected leaf path exactly once, not once per occurrence. The `inArray`\n // flag below also prevents nested array-in-array recursion from recording\n // duplicate entries at the same path (e.g. `{a: [[1,2],[3,4]]}`).\n const pollutedKeysSet = new Set<string>();\n\n // === Detachment invariant ===\n //\n // The single upfront `safeDeepClone` enforces maxKeyLength / maxArrayLength /\n // maxDepth and produces `cloned`, a tree fully detached from `input`:\n //\n // 1. No reference inside `cloned` aliases back into the caller-owned input\n // tree. This holds because safeDeepClone walks plain objects and arrays\n // recursively and only re-emits primitives / freshly-created containers.\n // 2. Cycles in `input` are broken by safeDeepClone's path-stack `WeakSet`\n // (a node currently on the recursion stack is replaced with `{}` / `[]`\n // on its second visit), so `cloned` is acyclic.\n //\n // processNode walks `cloned` (NOT `input`) and:\n // - records `node` (a reference INTO `cloned`) into the polluted tree,\n // which is safe precisely because (1) guarantees no caller-owned data\n // is exposed via the polluted tree, and\n // - rebuilds the cleaned output as a fresh structure (objects via the\n // `out` literal in this function; arrays via `Array.prototype.map`).\n //\n // No nested safeDeepClone is performed here — it would be redundant work\n // and, if invoked with a fresh WeakSet, could re-introduce traversal of\n // cycles that the upfront clone already broke. (See Finding 24 in FIX.md.)\n const cloned = safeDeepClone(input, opts.maxKeyLength, opts.maxArrayLength, opts.maxDepth);\n\n function processNode(node: unknown, path: string[] = [], depth = 0, inArray = false): unknown {\n if (node === null) return opts.preserveNull ? null : undefined;\n if (node === undefined) return node;\n\n if (Array.isArray(node)) {\n // Array is already truncated to maxArrayLength by the upfront clone, so\n // we can use it directly without re-slicing.\n //\n // Pollution is recorded only at the OUTERMOST array site for a given\n // `path`. When `inArray` is true, we are recursing into elements of an\n // already-recorded outer array (e.g. `{a: [[1,2],[3,4]]}` reaches the\n // inner arrays at the same `path` as the outer one); in that case we\n // skip the redundant `setIn` / `pollutedKeys.push` so consumers of\n // `pollutedKeys` and `pollutedTree` see one entry per affected leaf.\n const mapped = node.map((v) => processNode(v, path, depth, true));\n if (!inArray) {\n // Record pollution for ALL strategies (including combine). The combined\n // output remains the cleaned value, but the security signal — polluted\n // tree, pollutedKeys, onPollutionDetected callback — must still fire so\n // consumers of those signals are not silently bypassed in combine mode.\n //\n // Per the detachment invariant above, `node` is part of `cloned` (NOT\n // `input`); storing it in the polluted tree therefore cannot leak any\n // caller-owned reference, and the polluted tree is itself acyclic\n // because `cloned` is acyclic.\n setIn(polluted, path, node);\n pollutedKeysSet.add(path.join(\".\"));\n }\n return mergeValues(mapped, opts.mergeStrategy);\n }\n\n if (isPlainObject(node)) {\n /* istanbul ignore if -- defensive: safeDeepClone enforces the same depth limit first */\n if (depth > opts.maxDepth)\n throw new Error(`Maximum object depth (${opts.maxDepth}) exceeded`);\n const out: Record<string, unknown> = {};\n for (const rawKey of Object.keys(node)) {\n keyCount++;\n /* istanbul ignore if -- defensive: opts.maxKeys is always provided by callers */\n if (keyCount > (opts.maxKeys ?? Number.MAX_SAFE_INTEGER)) {\n throw new Error(`Maximum key count (${opts.maxKeys}) exceeded`);\n }\n const safeKey = sanitizeKey(rawKey, opts.maxKeyLength);\n /* istanbul ignore if -- defensive: keys already filtered by expandObjectPaths + safeDeepClone */\n if (!safeKey) continue;\n const child = (node as Record<string, unknown>)[rawKey];\n const childPath = path.concat([safeKey]);\n // Walking into an object key resets `inArray` — each key starts a fresh\n // path under which a new array site can record pollution exactly once.\n let value = processNode(child, childPath, depth + 1, false);\n if (typeof value === \"string\" && opts.trimValues) value = value.trim();\n out[safeKey] = value;\n }\n return out;\n }\n\n return node;\n }\n\n const cleaned = processNode(cloned, [], 0, false) as Record<string, unknown>;\n return { cleaned, pollutedTree: polluted, pollutedKeys: Array.from(pollutedKeysSet) };\n}\n\nexport function sanitize<T extends Record<string, unknown>>(\n input: T,\n options: SanitizeOptions = {},\n): T {\n validateSanitizeOptions(options);\n // Normalize and expand keys prior to sanitization\n const maxKeyLength = options.maxKeyLength ?? 200;\n const maxDepthVal = options.maxDepth ?? 20;\n const expandedInput = isPlainObject(input)\n ? expandObjectPaths(input, maxKeyLength, maxDepthVal)\n : input;\n const whitelist = normalizeWhitelist(options.whitelist);\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelist);\n const {\n mergeStrategy = DEFAULT_STRATEGY,\n maxDepth = 20,\n maxKeys = 5000,\n maxArrayLength = 1000,\n trimValues = false,\n preserveNull = true,\n } = options;\n\n // First: reduce arrays and collect polluted\n const { cleaned, pollutedTree } = detectAndReduce(expandedInput, {\n mergeStrategy,\n maxDepth,\n maxKeys,\n maxArrayLength,\n maxKeyLength,\n trimValues,\n preserveNull,\n });\n\n // Second: move back whitelisted arrays\n moveWhitelistedFromPolluted(cleaned, pollutedTree, isWhitelistedPath);\n\n return cleaned as T;\n}\n\ntype ExpressLikeNext = (err?: unknown) => void;\n\nfunction validateSanitizeOptions(options: SanitizeOptions): void {\n if (\n options.maxDepth !== undefined &&\n (typeof options.maxDepth !== \"number\" || options.maxDepth < 1 || options.maxDepth > 100)\n ) {\n throw new TypeError(\"maxDepth must be a number between 1 and 100\");\n }\n if (\n options.maxKeys !== undefined &&\n (typeof options.maxKeys !== \"number\" || options.maxKeys < 1)\n ) {\n throw new TypeError(\"maxKeys must be a positive number\");\n }\n if (\n options.maxArrayLength !== undefined &&\n (typeof options.maxArrayLength !== \"number\" || options.maxArrayLength < 1)\n ) {\n throw new TypeError(\"maxArrayLength must be a positive number\");\n }\n if (\n options.maxKeyLength !== undefined &&\n (typeof options.maxKeyLength !== \"number\" ||\n options.maxKeyLength < 1 ||\n options.maxKeyLength > 1000)\n ) {\n throw new TypeError(\"maxKeyLength must be a number between 1 and 1000\");\n }\n if (\n options.mergeStrategy !== undefined &&\n ![\"keepFirst\", \"keepLast\", \"combine\"].includes(options.mergeStrategy)\n ) {\n throw new TypeError(\"mergeStrategy must be 'keepFirst', 'keepLast', or 'combine'\");\n }\n if (options.trimValues !== undefined && typeof options.trimValues !== \"boolean\") {\n throw new TypeError(\"trimValues must be a boolean\");\n }\n if (options.preserveNull !== undefined && typeof options.preserveNull !== \"boolean\") {\n throw new TypeError(\"preserveNull must be a boolean\");\n }\n if (options.whitelist !== undefined) {\n if (typeof options.whitelist !== \"string\" && !Array.isArray(options.whitelist)) {\n throw new TypeError(\"whitelist must be a string or an array of strings\");\n }\n if (Array.isArray(options.whitelist)) {\n for (const entry of options.whitelist) {\n if (typeof entry !== \"string\") {\n throw new TypeError(\"whitelist must be a string or an array of strings\");\n }\n }\n }\n }\n}\n\nfunction validateOptions(options: HppxOptions): void {\n validateSanitizeOptions(options);\n if (options.sources !== undefined && !Array.isArray(options.sources)) {\n throw new TypeError(\"sources must be an array\");\n }\n if (options.sources !== undefined) {\n if (options.sources.length === 0) {\n throw new TypeError(\"sources must contain at least one of 'query', 'body', 'params'\");\n }\n for (const source of options.sources) {\n if (![\"query\", \"body\", \"params\"].includes(source)) {\n throw new TypeError(\"sources must only contain 'query', 'body', or 'params'\");\n }\n }\n }\n if (\n options.checkBodyContentType !== undefined &&\n ![\"urlencoded\", \"any\", \"none\"].includes(options.checkBodyContentType)\n ) {\n throw new TypeError(\"checkBodyContentType must be 'urlencoded', 'any', or 'none'\");\n }\n if (options.excludePaths !== undefined) {\n if (!Array.isArray(options.excludePaths)) {\n throw new TypeError(\"excludePaths must be an array\");\n }\n for (const entry of options.excludePaths) {\n if (typeof entry !== \"string\") {\n throw new TypeError(\"excludePaths must contain only strings\");\n }\n }\n }\n if (options.logger !== undefined && typeof options.logger !== \"function\") {\n throw new TypeError(\"logger must be a function\");\n }\n if (\n options.onPollutionDetected !== undefined &&\n typeof options.onPollutionDetected !== \"function\"\n ) {\n throw new TypeError(\"onPollutionDetected must be a function\");\n }\n if (options.strict !== undefined && typeof options.strict !== \"boolean\") {\n throw new TypeError(\"strict must be a boolean\");\n }\n if (options.logPollution !== undefined && typeof options.logPollution !== \"boolean\") {\n throw new TypeError(\"logPollution must be a boolean\");\n }\n}\n\nexport default function hppx(options: HppxOptions = {}) {\n // Validate options on middleware creation\n validateOptions(options);\n\n const {\n whitelist = [],\n mergeStrategy = DEFAULT_STRATEGY,\n sources = DEFAULT_SOURCES,\n checkBodyContentType = \"urlencoded\",\n excludePaths = [],\n maxDepth = 20,\n maxKeys = 5000,\n maxArrayLength = 1000,\n maxKeyLength = 200,\n trimValues = false,\n preserveNull = true,\n strict = false,\n onPollutionDetected,\n logger,\n logPollution = true,\n } = options;\n\n const whitelistArr = normalizeWhitelist(whitelist);\n const { isWhitelistedPath } = buildWhitelistHelpers(whitelistArr);\n\n return function hppxMiddleware(req: any, res: any, next: ExpressLikeNext) {\n try {\n // Read req.path defensively. Some upstream middleware decorates `req`\n // with a `path` getter that throws under specific conditions; if so,\n // that error must NOT propagate as a 500 — exclusion lookup is best\n // effort. On failure, treat the path as unknown (no exclusion match,\n // proceed to process the request normally) and surface a warning via\n // the configured logger so the upstream bug stays visible.\n let pathForExclusion: string | undefined;\n try {\n pathForExclusion = req?.path;\n } catch (pathErr) {\n const message = `[hppx] Failed to read req.path during exclusion check; proceeding without path-based exclusion. Underlying error: ${\n pathErr instanceof Error ? pathErr.message : String(pathErr)\n }`;\n if (logger) {\n try {\n logger(message);\n } catch (_) {\n console.warn(message);\n }\n } else {\n console.warn(message);\n }\n pathForExclusion = undefined;\n }\n if (shouldExcludePath(pathForExclusion, excludePaths)) return next();\n\n let anyPollutionDetected = false;\n const allPollutedKeys: string[] = [];\n\n // Per-request, per-source warning de-dup: don't spam the logger if writes fail\n // for both the cleaned source and the polluted-tree property on the same request.\n const warned = new Set<string>();\n const warn = (message: string) => {\n if (warned.has(message)) return;\n warned.add(message);\n if (logger) {\n try {\n logger(message);\n } catch (_) {\n // Logger failed; surface via console as a last-resort signal.\n console.warn(message);\n }\n } else {\n console.warn(message);\n }\n };\n\n for (const source of sources) {\n /* istanbul ignore next -- defensive: Express always invokes middleware\n with a non-null request object; this guard exists only so the loop\n degrades gracefully if a non-Express harness invokes the middleware\n with a missing/non-object req. */\n if (!req || typeof req !== \"object\") break;\n if (req[source] === undefined) continue;\n\n if (source === \"body\") {\n if (checkBodyContentType === \"none\") continue;\n if (checkBodyContentType === \"urlencoded\" && !isUrlEncodedContentType(req)) continue;\n }\n\n const part = req[source];\n if (!isPlainObject(part)) continue;\n\n // Preprocess: expand dotted and bracketed keys into nested objects\n const expandedPart = expandObjectPaths(part, maxKeyLength, maxDepth);\n\n const pollutedKey = `${source}Polluted`;\n const processedKey = `__hppxProcessed_${source}`;\n // Use hasOwnProperty.call to avoid prototype-chain traversal — protects against\n // upstream prototype pollution gadgets that set `Object.prototype.__hppxProcessed_*`.\n const hasProcessedBefore = Object.prototype.hasOwnProperty.call(req, processedKey);\n\n if (!hasProcessedBefore) {\n // First pass for this request part: reduce arrays and collect polluted\n const { cleaned, pollutedTree, pollutedKeys } = detectAndReduce(expandedPart, {\n mergeStrategy,\n maxDepth,\n maxKeys,\n maxArrayLength,\n maxKeyLength,\n trimValues,\n preserveNull,\n });\n\n // Express 5 exposes `req.query` only as a getter on the prototype chain (no\n // own descriptor by default), so the standard defineProperty path inside\n // setReqPropertySafe shadows it cleanly. If a downstream framework version\n // ever installs a non-configurable, non-writable descriptor, the helper\n // surfaces a clear warning instead of failing silently.\n setReqPropertySafe(req, source, cleaned, warn);\n\n // Attach polluted object (always present as {} when source processed)\n setReqPropertySafe(req, pollutedKey, pollutedTree, warn);\n // Mark as processed in a tamper-resistant, non-enumerable way so it is not\n // visible to user code, response serializers, or attackers.\n try {\n Object.defineProperty(req, processedKey, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n } catch (_) {\n // If req is frozen or defineProperty otherwise fails, fall back to assignment\n // so behavior is at least correct for the current request.\n try {\n req[processedKey] = true;\n } catch (_assignErr) {\n // Last resort: skip; downstream middleware will simply re-process.\n }\n }\n\n // Apply whitelist now: move whitelisted arrays back\n const sourceData = req[source];\n const pollutedData = req[pollutedKey];\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\n }\n\n if (pollutedKeys.length > 0) {\n anyPollutionDetected = true;\n for (const k of pollutedKeys) allPollutedKeys.push(`${source}.${k}`);\n }\n } else {\n // Subsequent middleware: only put back whitelisted entries\n const sourceData = req[source];\n const pollutedData = req[pollutedKey];\n if (isPlainObject(sourceData) && isPlainObject(pollutedData)) {\n moveWhitelistedFromPolluted(sourceData, pollutedData, isWhitelistedPath);\n }\n // pollution already accounted for in previous pass\n }\n }\n\n if (anyPollutionDetected) {\n // Log pollution detection if enabled\n if (logPollution) {\n const logMessage = `[hppx] HTTP Parameter Pollution detected - ${allPollutedKeys.length} parameter(s) affected: ${allPollutedKeys.join(\", \")}`;\n if (logger) {\n try {\n logger(logMessage);\n } catch (_) {\n // Fallback to console.warn if logger fails\n console.warn(logMessage);\n }\n } else {\n console.warn(logMessage);\n }\n }\n\n if (onPollutionDetected) {\n try {\n // Determine which sources had pollution\n for (const source of sources) {\n const pollutedKey = `${source}Polluted`;\n const pollutedData = req[pollutedKey];\n if (pollutedData && Object.keys(pollutedData).length > 0) {\n const sourcePollutedKeys = allPollutedKeys.filter((k) =>\n k.startsWith(`${source}.`),\n );\n if (sourcePollutedKeys.length > 0) {\n onPollutionDetected(req, {\n source: source,\n pollutedKeys: sourcePollutedKeys,\n });\n }\n }\n }\n } catch (_) {\n /* ignore user callback errors */\n }\n }\n if (strict && res && typeof res.status === \"function\") {\n return res.status(400).json({\n error: \"Bad Request\",\n message: \"HTTP Parameter Pollution detected\",\n pollutedParameters: allPollutedKeys,\n code: \"HPP_DETECTED\",\n });\n }\n }\n\n return next();\n } catch (err) {\n // Enhanced error handling with detailed logging\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (logger) {\n try {\n logger(error);\n } catch (logErr) {\n // If custom logger fails, surface via console.error so the developer\n // sees their logger bug regardless of NODE_ENV. Using `process.env`\n // directly here would crash in edge runtimes (Cloudflare Workers,\n // Vercel Edge, Deno without Node-compat) where `process` may be\n // undefined or `process.env` may be a throwing Proxy.\n console.error(\"[hppx] Logger failed:\", logErr);\n console.error(\"[hppx] Original error:\", error);\n }\n }\n\n // Pass error to next middleware for proper error handling\n return next(error);\n }\n };\n}\n\nexport { DANGEROUS_KEYS, DEFAULT_STRATEGY, DEFAULT_SOURCES };\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -8,6 +8,13 @@
8
8
  * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted
9
9
  * - TypeScript-first API
10
10
  */
11
+ declare module "express-serve-static-core" {
12
+ interface Request {
13
+ queryPolluted?: Record<string, unknown>;
14
+ bodyPolluted?: Record<string, unknown>;
15
+ paramsPolluted?: Record<string, unknown>;
16
+ }
17
+ }
11
18
  type RequestSource = "query" | "body" | "params";
12
19
  type MergeStrategy = "keepFirst" | "keepLast" | "combine";
13
20
  interface SanitizeOptions {
@@ -42,10 +49,16 @@ interface SanitizedResult<T> {
42
49
  declare const DEFAULT_SOURCES: RequestSource[];
43
50
  declare const DEFAULT_STRATEGY: MergeStrategy;
44
51
  declare const DANGEROUS_KEYS: Set<string>;
52
+ /**
53
+ * @internal — test-only helper that resets the module-level path segment cache.
54
+ * Public callers must not depend on this; it exists solely so tests can verify
55
+ * cache eviction behavior without exposing the internal Map.
56
+ */
57
+ declare function __resetPathSegmentCache(): void;
45
58
  declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
46
59
  type ExpressLikeNext = (err?: unknown) => void;
47
60
  declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
48
61
 
49
62
  // @ts-ignore
50
63
  export = hppx;
51
- export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, sanitize };
64
+ export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, __resetPathSegmentCache, sanitize };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,13 @@
8
8
  * - Exposes req.queryPolluted / req.bodyPolluted / req.paramsPolluted
9
9
  * - TypeScript-first API
10
10
  */
11
+ declare module "express-serve-static-core" {
12
+ interface Request {
13
+ queryPolluted?: Record<string, unknown>;
14
+ bodyPolluted?: Record<string, unknown>;
15
+ paramsPolluted?: Record<string, unknown>;
16
+ }
17
+ }
11
18
  type RequestSource = "query" | "body" | "params";
12
19
  type MergeStrategy = "keepFirst" | "keepLast" | "combine";
13
20
  interface SanitizeOptions {
@@ -42,8 +49,14 @@ interface SanitizedResult<T> {
42
49
  declare const DEFAULT_SOURCES: RequestSource[];
43
50
  declare const DEFAULT_STRATEGY: MergeStrategy;
44
51
  declare const DANGEROUS_KEYS: Set<string>;
52
+ /**
53
+ * @internal — test-only helper that resets the module-level path segment cache.
54
+ * Public callers must not depend on this; it exists solely so tests can verify
55
+ * cache eviction behavior without exposing the internal Map.
56
+ */
57
+ declare function __resetPathSegmentCache(): void;
45
58
  declare function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;
46
59
  type ExpressLikeNext = (err?: unknown) => void;
47
60
  declare function hppx(options?: HppxOptions): (req: any, res: any, next: ExpressLikeNext) => any;
48
61
 
49
- export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, hppx as default, sanitize };
62
+ export { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY, type HppxOptions, type MergeStrategy, type RequestSource, type SanitizeOptions, type SanitizedResult, __resetPathSegmentCache, hppx as default, sanitize };