How Prototype Pollution Happens in Node.js Package Managers and How to Fix It
Introduction
In a private Node.js application, the security scanning tool Trivy flagged a critical vulnerability in the dependency tree: CVE-2022-37601 affecting loader-utils versions 1.4.0 and 2.0.2. The issue wasn't in application code—it was hiding in a widely-used build utility that processes query strings and configuration parameters.
The vulnerability lies in the parseQuery.js file within loader-utils, specifically in how the parseQuery() function handles untrusted query parameters. Without proper validation, an attacker could craft a malicious query string that modifies JavaScript's Object.prototype, corrupting the prototype chain for all objects in the application's runtime. This is not a theoretical risk: prototype pollution has been weaponized in real-world attacks against Node.js applications.
This fix matters because loader-utils is a foundational package in the webpack ecosystem, meaning the vulnerability could affect any Node.js build pipeline, bundling process, or configuration loader that relies on it.
The Vulnerability Explained
What Is Prototype Pollution?
Prototype pollution is a vulnerability unique to dynamically-typed languages like JavaScript. Because JavaScript objects inherit properties from their prototypes, modifying Object.prototype affects every object in the entire application:
// Normal object behavior
const user = { name: "Alice", isAdmin: false };
// If an attacker pollutes Object.prototype:
// Object.prototype.isAdmin = true;
// Now ALL objects have isAdmin = true!
console.log(user.isAdmin); // true — vulnerability!
The Vulnerable Code Pattern
In loader-utils@1.4.0 and 2.0.2, the parseQuery() function processes URL query strings without sufficient validation. The vulnerable pattern looks like this:
// Simplified vulnerable code from parseQuery.js
function parseQuery(query) {
const obj = {};
// Parsing query string without prototype guards
query.split("&").forEach(pair => {
const [key, value] = pair.split("=");
obj[key] = decodeURIComponent(value); // No validation!
});
return obj;
}
// An attacker could send:
// ?__proto__[isAdmin]=true
// ?constructor[prototype][isAdmin]=true
// This would modify Object.prototype!
The issue is that parseQuery() accepts keys like __proto__, constructor, and prototype without filtering, allowing attackers to access and modify the prototype chain.
Real-World Attack Scenario
Consider a webpack configuration loader that uses loader-utils to parse build parameters:
// vulnerable build config loader
const loaderUtils = require('loader-utils');
const config = {};
// Attacker-controlled query string from a build webhook
const params = "?__proto__[requireUnsafe]=true&output=bundle.js";
const parsed = loaderUtils.parseQuery(params);
// Merge into config
Object.assign(config, parsed);
// Now Object.prototype is polluted:
// All objects have requireUnsafe = true
// An isAdmin check anywhere becomes true: {} instanceof Admin bypassed
Impact on This Application
In the context of bun.lock (Bun package manager lock file), loader-utils handles dependency resolution and build parameter parsing. A prototype pollution attack could:
- Bypass security checks: isAdmin, isAuthenticated, or similar boolean flags become true for all objects
- Trigger unintended code paths: Configuration-driven logic could be manipulated
- Cause denial of service: Polluting critical properties could crash or destabilize the build process
- Enable privilege escalation: In monorepo or CI/CD contexts, attackers could elevate privileges across the build pipeline
The Fix
The fix addresses CVE-2022-37601 by upgrading loader-utils across both dependency branches in the lock file:
Change 1: Direct Dependency Update
- "loader-utils": ["loader-utils@1.4.0", "", { "dependencies": { "big.js": "5.2.2", "emojis-list": "3.0.0", "json5": "1.0.2" } }, "sha512-..."],
+ "loader-utils": ["loader-utils@1.4.1", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "sha512-..."],
What changed:
- Version: 1.4.0 → 1.4.1
- Transitive dependencies now use caret ranges (^) instead of fixed versions
- Hash updated to reflect the new package content
Change 2: Nested Dependency Update
- "file-loader/loader-utils": ["loader-utils@2.0.2", "", { "dependencies": { "big.js": "5.2.2", "emojis-list": "3.0.0", "json5": "2.2.3" } }, "sha512-..."],
+ "file-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-..."],
What changed:
- Version: 2.0.2 → 2.0.4
- Dependency versions also updated with caret ranges
- Hash reflects the patched package
How the Patch Fixes the Issue
In loader-utils 1.4.1+ and 2.0.4+, the parseQuery() function now includes prototype pollution guards:
// Fixed version includes validation
function parseQuery(query) {
const obj = {};
query.split("&").forEach(pair => {
const [key, value] = pair.split("=");
// GUARD: Skip dangerous keys
if (key === "__proto__" ||
key === "constructor" ||
key === "prototype") {
return; // Silently skip, or throw error
}
obj[key] = decodeURIComponent(value);
});
return obj;
}
Security improvements:
1. Allowlist validation: Only safe property names are processed
2. Prototype chain protection: __proto__, constructor, and prototype keys are rejected
3. Tightened transitive dependencies: The caret ranges ensure that security patches to json5 and other deps are automatically included
4. Updated hashes: Prevents downgrade attacks by ensuring exact package verification
Prevention & Best Practices
1. Never Trust User Input in Object Keys
When parsing query strings, URL parameters, or JSON payloads, validate key names against an allowlist:
const ALLOWED_KEYS = ["page", "limit", "sort", "filter"];
const params = parseQueryString(userInput);
// Validate each key
Object.keys(params).forEach(key => {
if (!ALLOWED_KEYS.includes(key)) {
throw new Error(`Invalid parameter: ${key}`);
}
});
2. Use Secure Parsing Libraries
Instead of writing custom query parsers, use well-maintained libraries that handle prototype pollution:
// Good: Use built-in URL API with query parameter validation
const url = new URL(userInput);
const params = Object.fromEntries(url.searchParams);
// Good: Use qs library with options
const qs = require("qs");
const params = qs.parse(userInput, {
allowPrototypes: false // Prevents prototype pollution
});
3. Freeze Critical Objects
Prevent prototype modifications on sensitive objects:
const criticalConfig = Object.freeze({
apiKey: process.env.API_KEY,
isAdmin: false,
permissions: []
});
// Now Object.prototype.isAdmin = true won't affect criticalConfig
4. Keep Dependencies Updated
Use tools like npm audit, Snyk, or Trivy to detect and remediate vulnerable dependencies:
# Identify vulnerable packages
npm audit
# Fix with automated upgrades
npm audit fix
# Or use security scanning in CI/CD
trivy fs --severity CRITICAL .
5. Implement Property Validation
Check for dangerous property names during deserialization:
function safeDeserialize(obj) {
const DANGEROUS_PROPS = ["__proto__", "constructor", "prototype"];
for (const key of Object.keys(obj)) {
if (DANGEROUS_PROPS.includes(key)) {
delete obj[key]; // Remove dangerous properties
}
}
return obj;
}
6. Use CWE-1321 Awareness Tools
Leverage static analysis to catch prototype pollution:
# Semgrep rule for prototype pollution
semgrep -r . --config p/cwe-1321
7. Monitor for Prototype Pollution at Runtime
In production, monitor for suspicious property assignments:
// Log when Object.prototype is modified
const handler = {
set(target, prop, value) {
if (prop === "__proto__" || prop === "constructor") {
console.error(`Prototype pollution detected: ${prop}`);
// Alert security team
}
return Reflect.set(target, prop, value);
}
};
const proxiedObject = new Proxy({}, handler);
Key Takeaways
-
Prototype pollution in
parseQuery()corrupted objects across the entire runtime: The unvalidated key handling in loader-utils 1.4.0 and 2.0.2 allowed attackers to modifyObject.prototypethrough crafted query strings. -
The fix validates and rejects dangerous property names: Versions 1.4.1 and 2.0.4 implement allowlist-based filtering to prevent
__proto__,constructor, andprototypekeys from polluting the prototype chain. -
Dependency management matters for security: The upgrade also tightened transitive dependency constraints (json5, big.js, emojis-list) with caret ranges, ensuring security patches propagate automatically.
-
Lock file integrity is critical: The hash updates in
bun.lockensure that only the patched, verified versions are installed, preventing downgrade attacks. -
This vulnerability affects build pipelines and configuration loaders: Anywhere loader-utils processes untrusted input (query strings, build parameters, CI/CD webhooks), prototype pollution could have been exploited.
How Orbis AppSec Detected This
Source: Dependency version in bun.lock file specifying vulnerable loader-utils versions (1.4.0 and 2.0.2)
Sink: The parseQuery() function in parseQuery.js within loader-utils, which processes URL query strings without prototype pollution guards
Missing control: No validation of property keys to block prototype chain access; missing allowlist filtering for dangerous keys like __proto__, constructor, and prototype
CWE: CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes), related to CWE-94 (Improper Control of Generation of Code)
Fix: Upgraded loader-utils from 1.4.0 → 1.4.1 and 2.0.2 → 2.0.4 in bun.lock, which include prototype pollution guards in parseQuery() and tightened transitive dependency constraints
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Prototype pollution is a subtle but severe vulnerability that can compromise the integrity of an entire Node.js application. The CVE-2022-37601 vulnerability in loader-utils demonstrates how even foundational, widely-trusted packages can harbor dangerous flaws when they process untrusted input without proper safeguards.
By upgrading to patched versions (1.4.1 and 2.0.4), validating input keys against allowlists, and keeping dependencies current, development teams can eliminate this attack surface. The automated fix applied here—changing just two lines in bun.lock—shows how quickly prototype pollution can be remediated, but only if teams actively scan for and monitor vulnerable dependencies.
Remember: prototype pollution bypasses trust boundaries in your codebase. Treat any code that processes user-controlled property names as a potential attack vector, and always validate against known-safe patterns. Your application's security depends on it.
References
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes — https://cwe.mitre.org/data/definitions/1321.html
- CWE-94: Improper Control of Generation of Code ('Code Injection') — https://cwe.mitre.org/data/definitions/94.html
- OWASP Top 10 - A03:2021: Injection — https://owasp.org/Top10/A03_2021-Injection/
- Prototype Pollution in Node.js: HackerOne Report Series — https://hackerone.com/reports/310439
- Semgrep Rule for Prototype Pollution: https://semgrep.dev/r?q=prototype-pollution
- Node.js Security Best Practices: https://nodejs.org/en/docs/guides/security/
- npm audit Documentation: https://docs.npmjs.com/cli/v8/commands/npm-audit
- GitHub PR: fix: upgrade loader-utils to 2.0.3, 1.4.1 (CVE-2022-37601)