Skip to content
HipraxPublic

About

Superior HTTP Parameter Pollution protection middleware with modern TypeScript, robust sanitizer, and extensive tests.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

hppx

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.

npm version CI CodeQL codecov Dependencies npm provenance License: MIT Node.js


Features

  • Zero runtime dependencies — minimal attack surface and bundle size
  • Multiple merge strategies — keepFirst, keepLast (default), combine
  • Enhanced security:
    • Blocks dangerous keys: __proto__, prototype, constructor
    • Rejects control, bidirectional-override, and BOM characters in keys (null byte included)
    • Rejects malformed keys (dot/bracket-only patterns)
    • Validates key lengths to prevent DoS attacks
    • Limits array sizes to prevent memory exhaustion
  • Flexible whitelisting — nested whitelist with dot-notation, leaf, and prefix/subtree matching
  • Pollution tracking — records polluted parameters on the request (queryPolluted, bodyPolluted, paramsPolluted)
  • Multi-middleware support — works with multiple middlewares on different routes (whitelists applied incrementally)
  • DoS protection — maxDepth, maxKeys, maxArrayLength, maxKeyLength
  • Performance optimized — path caching and Set-based lookups for fast whitelist checks
  • Fully typed API — TypeScript-first with comprehensive type definitions for both ESM and CommonJS

Installation

npm install hppx

Upgrading to 0.5.0

Two behavior changes, both explained in the CHANGELOG:

  • A dotted or bracketed key whose expansion would create a $-prefixed key (password[$ne], password.$ne) is dropped instead of becoming { password: { $ne: ... } } (FAQ 11).
  • With trimValues: true, strings inside arrays kept by combine or whitelist are trimmed too.

Quick Start

ESM (ES Modules)

import express from "express";
import hppx from "hppx";

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(
  hppx({
    whitelist: ["tags", "user.roles", "ids"],
    mergeStrategy: "keepLast",
    sources: ["query", "body"],
  }),
);

app.get("/search", (req, res) => {
  res.json({
    query: req.query,
    queryPolluted: req.queryPolluted ?? {},
    body: req.body ?? {},
    bodyPolluted: req.bodyPolluted ?? {},
    params: req.params,
    paramsPolluted: req.paramsPolluted ?? {},
  });
});

CommonJS

const express = require("express");
const hppx = require("hppx");

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(
  hppx({
    whitelist: ["tags", "user.roles", "ids"],
    mergeStrategy: "keepLast",
    sources: ["query", "body"],
  }),
);

app.get("/search", (req, res) => {
  res.json({
    query: req.query,
    queryPolluted: req.queryPolluted ?? {},
    body: req.body ?? {},
    bodyPolluted: req.bodyPolluted ?? {},
    params: req.params,
    paramsPolluted: req.paramsPolluted ?? {},
  });
});

Polluted Parameter Tree

For each enabled source, hppx attaches a parallel *Polluted object to the request that records the original (pre-reduction, untrimmed even with trimValues) array values for any keys that were detected as polluted:

Source Cleaned data on req Polluted tree on req
query req.query req.queryPolluted
body req.body req.bodyPolluted
params req.params req.paramsPolluted

These properties are typed via a TypeScript module augmentation included in the published types — no extra import is needed.


API

Default Export: hppx(options?: HppxOptions)

Creates an Express-compatible middleware. Applies sanitization to each selected source and exposes *.Polluted objects on the request.

Note: Invalid options throw a TypeError at middleware creation time, not at request time. This ensures misconfiguration is caught early. Options are also captured at that point: changing the arrays you passed (sources, excludePaths, whitelist) or the exported DEFAULT_SOURCES after hppx() returns does not affect that middleware, and an invalid entry added to DEFAULT_SOURCES makes later hppx() calls that omit sources throw.

Options

Whitelist & Strategy:

Option Type Default Description
whitelist string[] | string [] Keys allowed to remain as arrays. Supports exact dot-notation ("user.tags"), leaf matching ("tags" matches any path ending in tags), and prefix/subtree matching ("user" whitelists every key under the user.* subtree).
mergeStrategy 'keepFirst' | 'keepLast' | 'combine' 'keepLast' How to reduce duplicate/array parameters when not whitelisted. keepFirst takes the first value, keepLast takes the last, combine flattens all values into a single array.

Source Selection:

Option Type Default Description
sources Array<'query' | 'body' | 'params'> ['query', 'body', 'params'] Which request parts to sanitize.
checkBodyContentType 'urlencoded' | 'any' | 'none' 'urlencoded' When to process req.body. urlencoded only processes URL-encoded bodies, any processes all content types, none skips body processing entirely.
excludePaths string[] [] Paths to exclude from sanitization. Supports * wildcard suffix (e.g., "/assets*").

Security Limits (DoS Protection):

Option Type Default Range Description
maxDepth number 20 1 - 100 Maximum object nesting depth. Exceeding this throws an error passed to next().
maxKeys number 5000 >= 1 Maximum number of keys to process. Exceeding this throws an error passed to next().
maxArrayLength number 1000 >= 1 Maximum array length. Arrays are truncated before processing.
maxKeyLength number 200 1 - 1000 Maximum key string length. Longer keys are silently dropped.

Note: A limit outside its range, a value that is not a number (undefined selects the default), or NaN (for example Number(process.env.UNSET) for an unset setting) throws a TypeError when the middleware is created, and sanitize() throws the same error. Infinity is accepted for maxKeys and maxArrayLength, which have no upper bound.

Note — in-order, in-place commit model: Sources are processed in the order specified by the sources array and each source's sanitized result is committed in-place to req before the next source begins. When maxDepth or maxKeys is exceeded, the error is forwarded to next() immediately — but any earlier sources that already completed are already sanitized on req while the throwing source stays raw. Error handlers should not assume an atomic all-or-nothing transform across sources.

Behavior & Callbacks:

Option Type Default Description
trimValues boolean false Trim whitespace from every string in the sanitized output, including elements of arrays kept by combine or whitelist. req.*Polluted keeps the original values.
preserveNull boolean true Preserve null values in the output.
strict boolean false Immediately respond with HTTP 400 when pollution is detected. Response includes error, message, pollutedParameters, and code ("HPP_DETECTED") fields.
onPollutionDetected (req, info) => void — Callback fired on pollution detection. Called once per polluted source (e.g., fires twice if both query and body are polluted). info contains { source: RequestSource, pollutedKeys: string[] }.
logger (err: Error | unknown) => void — Custom logger for errors and pollution warnings. Receives string for pollution warnings and Error for caught errors. Falls back to console.warn/console.error if the logger throws. Without a logger, pollution warnings go to console.warn and hppx does not log errors itself (see Error Handling).
logPollution boolean true Enable automatic logging when pollution is detected.

Named Export: sanitize(input, options?)

function sanitize<T extends Record<string, unknown>>(input: T, options?: SanitizeOptions): T;

Sanitize a plain object using the same rules as the middleware.

Return shape: sanitize() returns only the cleaned object — the same shape as input, with arrays reduced according to the chosen merge strategy. It does not return the internal SanitizedResult ({ cleaned, pollutedTree, pollutedKeys }); polluted-tree data is deliberately discarded. If you need access to the polluted tree or polluted keys, use the middleware factory hppx() and read req.queryPolluted / req.bodyPolluted / req.paramsPolluted instead.

Options: sanitize() accepts only SanitizeOptions — whitelist, mergeStrategy, maxDepth, maxKeys, maxArrayLength, maxKeyLength, trimValues, and preserveNull. Middleware-only options (sources, excludePaths, strict, onPollutionDetected, logger, logPollution, checkBodyContentType) are silently ignored when passed to sanitize() — use hppx() if you need any of those features.

ESM:

import { sanitize } from "hppx";

const clean = sanitize(payload, {
  whitelist: ["user.tags"],
  mergeStrategy: "keepFirst",
});

CommonJS:

const { sanitize } = require("hppx");

const clean = sanitize(payload, {
  whitelist: ["user.tags"],
  mergeStrategy: "keepFirst",
});

Exported Types

All types are available for both ESM and CommonJS consumers. CommonJS projects on TypeScript 7 need hppx 0.5.0 or later: earlier declarations relied on a leniency that TypeScript 7 removed, so named imports such as import { sanitize } from "hppx" failed there.

import type {
  RequestSource, // "query" | "body" | "params"
  MergeStrategy, // "keepFirst" | "keepLast" | "combine"
  SanitizeOptions, // Options for sanitize()
  HppxOptions, // Full middleware options (extends SanitizeOptions)
  SanitizedResult, // { cleaned, pollutedTree, pollutedKeys }
} from "hppx";

Exported Constants

import { DANGEROUS_KEYS, DEFAULT_SOURCES, DEFAULT_STRATEGY } from "hppx";

DANGEROUS_KEYS; // ReadonlySet<string>: "__proto__", "prototype", "constructor" (a copy for inspection)
DEFAULT_SOURCES; // ["query", "body", "params"]
DEFAULT_STRATEGY; // "keepLast"

Note: DANGEROUS_KEYS is a copy of the keys hppx blocks. Its own add, delete and clear throw a TypeError, but JavaScript cannot freeze the contents of a Set: calling Set.prototype.delete or Set.prototype.add on it directly still changes the copy. That never affects hppx, whose guards use a private list, so treat the export as information and do not build your own security checks on it.


Advanced Usage

Strict Mode (Respond 400 on Pollution)

app.use(hppx({ strict: true }));

// Polluted requests receive:
// {
//   "error": "Bad Request",
//   "message": "HTTP Parameter Pollution detected",
//   "pollutedParameters": ["query.x"],
//   "code": "HPP_DETECTED"
// }

Process JSON Bodies Too

app.use(express.json());
app.use(hppx({ checkBodyContentType: "any" }));

Exclude Specific Paths

app.use(hppx({ excludePaths: ["/public", "/assets*"] }));

Custom Logging

// Use your application's logger
app.use(
  hppx({
    logger: (msg) => {
      if (typeof msg === "string") {
        myLogger.warn(msg); // Pollution warnings
      } else {
        myLogger.error(msg); // Errors
      }
    },
  }),
);

// Disable automatic pollution logging
app.use(hppx({ logPollution: false }));

Error Handling

hppx passes an error to next(err) when a request exceeds maxDepth or maxKeys, and when anything else fails while it processes a request (a thrown value that is not an Error is wrapped in one). Invalid options never reach a request: they throw a TypeError when hppx() is called.

  • hppx logs these errors only through logger. Without one it writes nothing, and the error handler that receives the error decides: Express's default handler logs it (outside NODE_ENV=test), and a custom handler should log it itself. Logging in both places would write every error twice.
  • They are plain Error objects without a status, so Express answers 500 unless your handler maps them. The limit errors are caused by the request, so a 400 is usually the better answer:
import type { ErrorRequestHandler } from "express";

const hppLimitErrors: ErrorRequestHandler = (err, _req, res, next) => {
  if (
    err instanceof Error &&
    /^Maximum (object depth|key count) \([^)]*\) exceeded$/.test(err.message)
  ) {
    res.status(400).json({ error: "Bad Request" });
    return;
  }
  next(err);
};

app.use(hppx({ maxDepth: 10, maxKeys: 500 }));
// ... routes ...
app.use(hppLimitErrors);

Sources processed before the failing one are already sanitized on req (see the in-order commit note under Security Limits).

Multi-Middleware Stacking

hppx supports incremental whitelisting across multiple middleware instances. Each subsequent middleware applies its own whitelist to the already-collected polluted data:

// Global middleware — whitelist "a"
app.use(hppx({ whitelist: ["a"] }));

// Route-level middleware — additionally whitelist "b" and "c"
const router = express.Router();
router.use(hppx({ whitelist: ["b", "c"] }));

// On this route, "a", "b", and "c" are all allowed as arrays
router.get("/data", (req, res) => {
  res.json({ query: req.query });
});

app.use("/api", router);

Option Precedence Across Stacked Middleware

When the same source (query / body / params) has already been processed by an earlier hppx() instance on the same request, a subsequent hppx() only applies its own whitelist — used to restore additional whitelisted entries from the polluted tree the first middleware already collected. Every other option on the later instance is silently ignored for that source because the source is no longer available in its original (un-reduced) form.

The options ignored on subsequent middleware (per-source) are:

  • mergeStrategy
  • maxDepth, maxKeys, maxArrayLength, maxKeyLength
  • trimValues, preserveNull
  • strict (will not trigger HTTP 400 if the earlier middleware already cleaned the source)
  • onPollutionDetected, logger, logPollution
  • excludePaths is checked per-instance (independent of the processed flag), but if the source was already processed, only whitelist restoration runs.

Whitelisted values that a later instance restores follow the first instance's trimValues: they are trimmed when the instance that processed the source trims, and left as they arrived otherwise. (If the request object refuses the internal flag that records the setting, restored values stay untrimmed.)

Footgun example:

// Global middleware — keepLast strategy, no strict mode
app.use(hppx({ mergeStrategy: "keepLast" }));

// Route-level middleware — strict mode, but it's TOO LATE
app.use(
  "/api/admin",
  hppx({ strict: true }), // SILENTLY IGNORED — the source was already
  // cleaned by the global middleware, so strict
  // mode here will NOT cause a 400 response.
);

If you need strict mode on a specific route, configure strict: true on the first hppx() instance that processes the relevant source — typically the global middleware. Equivalent options (maxDepth, mergeStrategy, callbacks, loggers) must likewise be set on the first instance. Subsequent instances are useful only for expanding the whitelist to recover additional fields from req.queryPolluted/req.bodyPolluted/req.paramsPolluted on a per-route basis.

A subsequent middleware can only ever expand the whitelist (by restoring more fields back from the polluted tree). It cannot restrict an already-whitelisted field, because the previous middleware has already moved that field back into the source.

Pollution Detection Callback

app.use(
  hppx({
    onPollutionDetected: (req, info) => {
      // Called once per polluted source (query, body, params)
      securityLogger.warn("HPP detected", {
        source: info.source,
        pollutedKeys: info.pollutedKeys,
      });
    },
  }),
);

Security

What hppx Protects Against

Threat Protection
Parameter pollution Duplicate parameters, including a parameter repeated under alternate key spellings, are reduced to a single value via the chosen merge strategy
Alternate-syntax duplicates Spellings that expand to the same key (a / a[] / a. / [a], a.b / a[b]) are combined and reported as duplicates, so strict mode rejects them. A structural conflict (one spelling assigns a value directly to a key, another nests keys under it) is not reported and replaces the earlier value, which is then not combined with later spellings of that key (FAQ 9)
Prototype pollution __proto__, constructor, prototype keys are blocked at every processing level, from a private list that other code cannot change (the exported DANGEROUS_KEYS is a copy for inspection)
$ keys created by expansion Key expansion never creates a key that starts with $: password[$ne]=x or password.$ne=x is dropped instead of becoming { password: { $ne: "x" } }. Keys that the parser or a JSON body already nested are passed through (FAQ 11)
DoS via deep nesting maxDepth limit throws error on excessive nesting
DoS via key flooding maxKeys limit throws error when key count is exceeded
DoS via large arrays maxArrayLength truncates arrays before processing
DoS via long keys maxKeyLength silently drops excessively long keys
Null-byte injection Keys containing \u0000 are silently dropped
Control / bidi key chars Keys containing ASCII / C1 control characters (\x00-\x1F, \x7F-\x9F) or Unicode bidirectional override characters (LRM/RLM, LRE/RLE/PDF/LRO/RLO, LRI/RLI/FSI/PDI, BOM) are dropped
Frozen or poisoned prototype hppx writes own data properties, so Object.freeze(Object.prototype) or an inherited setter cannot make a key such as toString fail the request or divert its value (with Express's default query parser; FAQ 10)
Malformed keys Keys consisting only of dots/brackets (e.g., "...", "[[") are dropped

Production Configuration

app.use(
  hppx({
    maxDepth: 10,
    maxKeys: 1000,
    maxArrayLength: 100,
    maxKeyLength: 100,
    strict: true,
    onPollutionDetected: (req, info) => {
      securityLogger.warn("HPP detected", {
        ip: req.ip,
        path: req.path,
        source: info.source,
        pollutedKeys: info.pollutedKeys,
      });
    },
  }),
);

Express 5 Note

In Express 5, req.query is exposed as a lazy getter on the prototype chain rather than an own property. hppx handles this transparently: it uses Object.defineProperty to install the sanitized value as a writable own property that shadows the proto-level getter. After the middleware runs, req.query reflects the cleaned value (e.g. req.query.x === "2" after ?x=1&x=2 with mergeStrategy: "keepLast").

If a downstream layer makes req.query non-configurable AND non-writable before hppx runs (uncommon), hppx will not silently leave the polluted value in place — it will emit a warning via the configured logger (or console.warn if none is provided) so the misconfiguration is visible. The warning is de-duplicated per request and per source.

What hppx Does NOT Protect Against

hppx is not a complete security solution. You still need:

  • SQL injection protection — use parameterized queries
  • XSS protection — sanitize output, use CSP headers
  • CSRF protection — use CSRF tokens
  • Authentication/Authorization — validate user permissions
  • Rate limiting — prevent brute-force attacks
  • Input validation — use schema validation libraries (Joi, Yup, Zod) alongside hppx
  • NoSQL / ORM operator injection: any parameter can still arrive as an object. Express's extended query parser nests password[$ne] itself, a JSON body can send { "password": { "$ne": "x" } } directly, and hppx expands keys without a $ (id[not]=0 becomes { id: { not: "0" } }). Validate and cast every value before it reaches a query (a schema, or String(req.query.email)). With Mongoose, sanitizeFilter wraps objects with $-prefixed keys in $eq, and mongoose.trusted() marks the operators your own code adds

FAQ / Known Behaviors

A short reference for behaviors that surprise people most often.

1. The combine strategy still records pollution. Earlier versions silently dropped pollution events when mergeStrategy: "combine" was in effect, because the array was preserved as-is. This was a footgun for security logging. Today, combine records polluted keys into req.queryPolluted (etc.) and fires onPollutionDetected and logPollution exactly like the other strategies — the cleaned data simply contains the flattened array rather than a reduced single value.

2. Multi-middleware: subsequent passes only honor whitelist. When two hppx() instances run on the same request (e.g. global + router), the second one only applies its own whitelist — to restore additional fields out of the polluted tree the first instance already collected. All other options (mergeStrategy, strict, onPollutionDetected, limits, etc.) on the second instance are silently ignored for any source the first instance already processed. See Multi-Middleware Stacking → Option Precedence above for the full list. Configure strict: true, callbacks, and limits on the first hppx() that processes the source — typically the global middleware.

3. Express 5 frozen req.query fallback. Express 5 exposes req.query as a lazy getter on the prototype chain. hppx shadows it with a writable own property carrying the cleaned value. If an unusual downstream layer makes req.query non-configurable AND non-writable before hppx runs, hppx will emit a warning via the configured logger (or console.warn) instead of silently leaving the polluted array in place.

4. Control / bidirectional override characters in keys are rejected. Keys containing ASCII / C1 control characters (\x00-\x1F, \x7F-\x9F), Unicode bidirectional override characters (LRM/RLM, LRE/RLE/PDF/LRO/RLO, LRI/RLI/FSI/PDI), or BOM (\uFEFF) are silently dropped. This prevents log-injection / DB-corruption tricks that use invisible control characters to disguise key names.

5. sanitize() returns only the cleaned object. The standalone sanitize() function returns the same shape as its input, with arrays reduced. It does not return {cleaned, pollutedTree, pollutedKeys} — if you need the polluted tree, use the middleware factory hppx() and read req.queryPolluted / req.bodyPolluted / req.paramsPolluted. Middleware-only options (sources, excludePaths, strict, callbacks, etc.) are silently ignored when passed to sanitize().

6. whitelist is a data-preservation control; strict / logPollution are wire-level detection signals.

These two concerns are deliberately independent:

  • whitelist controls what happens after reduction. Whitelisted keys have their raw arrays (with their strings trimmed when trimValues is on) moved back from the polluted tree into req.query (etc.) and are pruned from req.queryPolluted. The route handler sees the original multi-value array for whitelisted keys — they are not further reduced.
  • strict and logPollution are driven by pre-restoration data — the pollutedKeys set returned by detectAndReduce captures every parameter that arrived duplicated on the wire (except the structural conflicts described in FAQ 9), regardless of whitelist configuration. Because anyPollutionDetected is set from that pre-restoration snapshot, a whitelisted key that arrives duplicated on the wire still causes strict: true to return HTTP 400 and logPollution: true to emit a warning.

If you need certain keys to carry multiple values without triggering strict mode, do not use strict: true in combination with whitelist for those keys — or handle those keys in a separate, non-strict middleware.

7. onPollutionDetected and req.*Polluted reflect the post-restoration state.

After detectAndReduce collects all duplicated keys, moveWhitelistedFromPolluted restores whitelisted entries back into req.query (etc.) and prunes them from req.queryPolluted. Two consequences:

  • req.queryPolluted (and req.bodyPolluted, req.paramsPolluted) never contains whitelisted keys — they have already been moved back to the main request object.
  • onPollutionDetected is invoked only when req.queryPolluted is non-empty after restoration. If all polluted keys are whitelisted, the polluted tree is {} after restoration and the callback is not called (even though logPollution still fires for the wire-level signal). If some (but not all) keys are whitelisted, the callback fires for that source — and its info.pollutedKeys array contains all pre-restoration polluted keys (both whitelisted and non-whitelisted ones).

8. Express 5 wildcard route params are arrays.

Express 5 delivers a wildcard (splat) route param as an array of path segments: with app.get("/files/*filepath", ...), GET /files/a/b/c.txt gives req.params.filepath as ["a", "b", "c.txt"]. hppx cannot tell a framework-built array from an injected duplicate, so it treats the splat like any other array-valued parameter:

  • A route-level hppx() with the default sources reduces it (keepLast leaves req.params.filepath === "c.txt"), keeps the full array in req.paramsPolluted, and reports params.filepath to onPollutionDetected and the pollution log. A required *name wildcard always yields an array, so a single-segment path such as /files/c.txt (["c.txt"]) is flagged too.
  • With strict: true, that route-level instance rejects every request that matches a required wildcard route with HTTP 400 (pollutedParameters: ["params.filepath"]).
  • Workaround: pass sources: ["query", "body"] to the hppx() on wildcard routes. The splat array reaches the handler intact, and duplicated query parameters are still reduced, or rejected in strict mode. Body handling is unchanged (it still follows checkBodyContentType).
  • A global app.use(hppx()) runs before routing, when req.params is still an empty object, so it never sees route params and leaves the splat array untouched. Because it marks params as processed for that request, a later route-level hppx() on the same request only restores its whitelist and neither reduces nor rejects the splat (see FAQ 2).

9. Keys that normalize to the same path are duplicates.

hppx expands dotted and bracketed keys into nested objects before it looks for duplicates, and spellings that expand to the same path count as a repeated parameter: a, a[], a. and [a] all name a; a.b, a[b] and a nested { a: { b: ... } } all name a.b. This matters most under Express 5's default "simple" query parser, which keeps ?a=1&a[]=2 as two separate keys (a and a[]), but it applies to every source and to sanitize().

  • The values are combined into one array, which is then treated like any duplicate: the merge strategy reduces it (for ?a=1&a[]=2, keepLast gives "2", keepFirst gives "1" and combine gives ["1", "2"]), the array is recorded in req.queryPolluted (etc.) and reported to onPollutionDetected and the pollution log, and strict: true rejects the request with HTTP 400 (?user.role=user&user[role]=admin is reported as query.user.role).
  • A whitelist entry for the key keeps the combined array, as it does for a repeated parameter. As FAQ 6 explains, strict and logPollution still react to the duplicate.
  • Values are combined in the order in which the parser presents the keys, which is not always wire order. The simple parser groups repeats of the same spelling, so ?a=1&b=x&a[]=2&a=3 arrives as { a: ["1", "3"], b: "x", "a[]": "2" } and combines to ["1", "3", "2"].
  • The limits apply to the combined array as to any array: maxArrayLength truncates it before reduction, so when it is longer than the limit keepLast can keep an earlier spelling; and a nested duplicate whose path is exactly maxDepth + 1 keys long throws the depth error, because the combined array adds a nesting level (an exact duplicate at that depth already did).
  • Plain objects reached through different spellings are merged instead of the later one replacing the earlier: { "a.b": "1", a: { c: "2" } } gives { a: { b: "1", c: "2" } }.
  • A structural conflict is not a duplicate. When one spelling assigns a value directly to a key and another nests keys under it (a and a.b), the last-processed shape wins: { a: "1", "a.b": "2" } gives { a: { b: "2" } }, the reverse key order gives { a: "1" }. The conflict is not reported, and the value it replaces is not combined with later spellings of the same key, so the outcome depends on key order.

10. hppx works with a frozen Object.prototype. Freezing Object.prototype (a common prototype-pollution mitigation) makes every inherited method name read-only, and a plain assignment such as out.toString = value then throws. hppx writes each key of the objects it builds as an own data property, the way JSON.parse does, so ?toString=1 or ?valueOf=a&valueOf=b are sanitized like any other key. Before 0.4.0 such a request failed with a TypeError (a 500 in Express) whenever Object.prototype was frozen. This covers hppx's own processing: Express's extended query parser (qs) makes the same assignment itself and fails before hppx runs, while the default simple parser does not.

11. Key expansion never creates $ operator keys. MongoDB and similar stores read keys that start with $ as query operators. Express 5's default simple query parser delivers ?password[$ne]=x as the flat key "password[$ne]", harmless on its own, so hppx drops every dotted or bracketed key whose expansion would contain a segment starting with $ (password[$ne], password.$ne, user[role][$in], [$ne], $where.) before any part of it is written. Versions before 0.5.0 turned it into { password: { $ne: "x" } }.

  • This applies to every source and to sanitize(). A dropped key is not reported as pollution (no *Polluted entry, no strict rejection), just as with the blocked prototype keys.
  • Only a leading $ counts: a[b$] is expanded as usual.
  • Keys that expansion does not create are left alone: a flat $top or $filter (OData-style parameters), and operators that the extended parser or a JSON body already nested (?password[$ne]=x under extended still gives { password: { $ne: "x" } }; see "What hppx Does NOT Protect Against").
  • FAQ 9 treats a and a[] as one parameter. For $ names that no longer holds: under the default simple parser, ?$top=1&$top[]=2 drops $top[] and $top stays "1" (the extended parser merges both spellings into an array itself).
  • If you rely on operator syntax in query strings, use the extended parser, which nests brackets itself, and validate the result.

License

MIT License - see LICENSE file for details.

Links

About

Superior HTTP Parameter Pollution protection middleware with modern TypeScript, robust sanitizer, and extensive tests.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages