← Back to case studies
CVE-2025-55164 CVSS 8.8 High JavaScript · TypeScript

@helmetjs/content-security-policy-parser — Prototype Pollution via Plain Object CSP Parsing

The CSP parser used by Helmet.js stored parsed directives in a plain JavaScript object — allowing an attacker to supply __proto__ as a directive name and pollute Object.prototype, affecting every object in the Node.js process.

CVSS 8.8 / 10.0
CWE CWE-1321 ( Prototype Pollution )
Project @helmetjs/content-security-policy-parser (Helmet.js ecosystem)
Published August 2025
Fix commit b13a52554f0168af393e3e38ed4a94e9e6aea9dc
// what happened
  • Affected: @helmetjs/content-security-policy-parser < 0.6.0 — CSP parser used by Helmet.js, one of the most widely deployed security middleware packages for Express/Node.js. As of publication, only 17% of weekly npm downloads were on the patched version.
  • Attack vector: The parser stores CSP directive key-value pairs in a plain JavaScript object ({}). Supplying __proto__ as a directive name in the CSP header causes result['__proto__'] to write to Object.prototype — polluting the global prototype chain for every object in the Node.js process.
  • Requires: Network access to a server that processes a CSP header containing __proto__ in a directive name. No authentication.
  • Impact: DoS (applications relying on Object.prototype property checks fail), secondary exploitation (pollution enables subsequent gadget-chain attacks in other libraries that consume the parsed result). No RCE in isolation, but opens system-wide attack surface.
  • Fix: Replace plain {} with Map(). Map is unaffected by __proto__ key injection — __proto__ is treated as a literal string key. Fix commit: helmetjs/content-security-policy-parser@b13a525.

Base Score
8.8 High
Attack Vector (AV)
Network — attacker sends a crafted CSP header in an HTTP request
Attack Complexity (AC)
Low — single POST with __proto__ in header; no conditions to meet
Privileges Required (PR)
None — no credentials needed; CSP header is HTTP-level
User Interaction (UI)
None — server processes the header automatically
Scope (S)
Unchanged
Confidentiality (C)
None
Integrity (I)
High — prototype pollution affects every object in the process
Availability (A)
High — DoS via polluted properties breaking application logic
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H → 8.8

index.ts — CSP directive parsing with plain object Pre-fix — before commit b13a525 // CSP parser — converts a CSP header string to a key-value map. // Used downstream by Helmet.js to set security headers. function parseCSP(policy: string): Record { const result: Record = {}; // VULNERABLE: Using a plain object — __proto__ key pollutes Object.prototype. // A CSP header like "default-src 'self'; __proto__; xss-script; img-src *" // sets result['__proto__'] which actually writes to Object.prototype. // After parsing, every object in the Node.js process has 'xss-script' → ['img-src', '*']. // // Exploit: attacker sets customer name to "{{__proto__}}" or sends header with // __proto__ as a directive key. Any downstream code doing: // for (const key in parseCSP(header)) { ... } // or: Object.assign({}, parseCSP(header)) // inherits the polluted keys. policy.split(';').forEach(directive => { const [key, ...values] = directive.trim().split(/\//g); if (key && !result[key]) { // key='__proto__' bypasses this check result[key] = values; } }); return result; // contains polluted keys in Object.prototype }

Why plain objects are vulnerable to prototype pollution: In JavaScript, obj['__proto__'] accesses the object's prototype (not a string key). Object.prototype is shared across all objects. Writing to Object.prototype.isAdmin makes ({}).isAdmin === true for every object — including user-facing data structures, configuration objects, and library internals.

From crafted CSP header to prototype chain pollution
1
Attacker identifies a server using @helmetjs/content-security-policy-parser < 0.6.0. The package is a transitive dependency of helmet, so any Express app with Helmet middleware is potentially affected. Run npm list helmet to check.
2
Attacker sends an HTTP request with a crafted Content-Security-Policy header: CSP: default-src 'self'; __proto__; eval; img-src * The __proto__ directive writes to Object.prototype.eval['img-src', '*'].
3
Parser returns result which contains Object.prototype pollution. Any code that iterates the result object (via for...in, Object.keys(), Object.assign(), or spread syntax) inherits the attacker's keys.
4
Denial of service — application logic breaks. Example: if (!user.isAdmin) becomes if (!(true)) → always false, privilege checks fail silently. Or user.profile returns the attacker's ['img-src', '*'] instead of the user's profile object.
5
Gadget chain exploitation — libraries that read from the polluted objects and use those values in dangerous operations (eval, new Function, dynamic property access) can escalate prototype pollution to full RCE.

Fix commit helmetjs/content-security-policy-parser@b13a525 — plain object replaced with Map
// CHANGE: Use Map() instead of a plain object for storing parsed directives. // Map does not have a prototype chain — __proto__ is a literal string key. // Setting map.set('__proto__', ['foo']) stores 'foo' for the key '__proto__', // but does NOT affect Object.prototype. type ParsedCSP = Map; // Old: function parseCSP(policy: string): Record // New return type: export default function parseCSP(policy: string): ParsedCSP { const result: ParsedCSP = new Map(); policy.split(';').forEach(directive => { const [key, ...values] = directive.trim().split(/\//g); if (key && !result.has(key)) { result.set(key, values); // Map.set() treats '__proto__' as a literal key } }); return result; // Iteration only yields actual directive keys } // Usage change: callers must update from obj['default-src'] to map.get('default-src') // Test updated: "parsing __proto__ as a directive" now expects Map with '__proto__' as // a literal key entry, not prototype pollution.

Why this works: Map has no prototype chain in the way plain objects do. map.set('__proto__', value) stores '__proto__' as a literal string key — it does not pollute Object.prototype. Iteration via map.forEach() or for...of only sees the actual stored keys.

Breaking change: Callers of parseCSP() must update from object property access (result['default-src']) to Map methods (result.get('default-src')).


🟠 PullLight — High Finding (CVSS 8.8)
[HIGH] Prototype Pollution (CWE-1321) — index.ts parseCSP()

The CSP parser uses a plain JavaScript object ({}) to store parsed directives. Supplying __proto__ as a directive name in a CSP header writes to Object.prototype, polluting the global prototype chain. Any downstream code that merges, spreads, or iterates the parsed result is affected.

This is CWE-1321 (Prototype Pollution) — specifically, unsanitized __proto__ and constructor keys being written to a plain object in parsing code. The vulnerable pattern (plain object + string split) is a common template for parsers — PullLight detects it across all languages.

CVSS 8.8: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H — no-gadget RCE in this library alone, but DoS (17% of downloads affected, only 17% patched) and gadget chain exploitation possible downstream.
→ Fix: Replace {} with Map() for storing parsed CSP directives. Map is unaffected by __proto__ injection. Audit all code consuming the parsed result for unsafe object operations (spread, Object.assign, for...in without hasOwnProperty). Fix commit: helmetjs/content-security-policy-parser@b13a525.

// root cause
The parser was designed without accounting for JavaScript's prototype chain behavior. Plain objects in JavaScript share Object.prototype — writing to obj['__proto__'] or obj['constructor'] affects every object in the runtime. When parsing CSP headers (attacker-controlled input), the code used a plain object without validating whether directive keys were prototype-reserved properties.

The fix (Map) is simple but fundamental: Map does not share a prototype chain, so __proto__ is treated as a literal string key. This is the correct data structure for key-value storage when keys come from untrusted input.

Only 17% of weekly downloads are on the patched version — indicating slow uptake. The advisory was published August 2025, but the fix was committed in February 2024, meaning the vulnerability sat unpatched for 18 months before public disclosure.

// what you can do

What's the difference between prototype pollution and object injection?
Object injection refers to attacker-controlled properties being set on a specific object. Prototype pollution is a specific case where the attacker's values are written to Object.prototype, affecting every object in the JavaScript runtime. Prototype pollution is a special case of object injection with system-wide impact.
Can this be exploited for RCE?
In isolation, no — this library's parser doesn't execute code. But prototype pollution enables "gadget chains": other libraries that read from the polluted objects and use those values in dangerous operations (like eval, new Function(), or dynamic property access). If your application uses libraries with known gadget chains, prototype pollution can escalate to RCE.
How does the attacker send a CSP header?
CSP headers can be set by a MitM attacker on HTTP connections. In server-side CSP parsing, the header value comes from the HTTP request — which is attacker-controlled on non-HTTPS connections. Additionally, some server configurations use CSP values from database or config files that users can influence.
Why is uptake so low (17% patched)?
The fix was committed in February 2024 but not disclosed until August 2025 (18 months of silence). During that window, only 17% of weekly downloads were patched. The advisory publication is now driving uptake — but millions of installs remain vulnerable.

February 2024 Fix commit b13a52554f0168af393e3e38ed4a94e9e6aea9dc lands privately — plain object replaced with Map, prototype pollution eliminated.
August 2025 CVE-2025-55164 assigned. Security advisory GHSA-w2cq-g8g3-m83 published by EvanHahn (maintainer). Only 17% of weekly npm downloads on patched version — widespread exposure.
June 2026 PullLight documents CVE-2025-55164 as case study #30 — demonstrating prototype pollution (CWE-1321) detection via plain object usage analysis in PR code review.

Don't let prototype pollution slip into your PRs

More CVE Case Studies