Back to Blog
critical SEVERITY8 min read

How Prototype Pollution happens in Node.js package managers and how to fix it

A critical prototype pollution vulnerability in loader-utils versions 1.4.0 and 2.0.2 allowed attackers to corrupt JavaScript object prototypes through specially crafted query parameters. The fix upgrades loader-utils to patched versions 1.4.1 and 2.0.4, which sanitize the parseQuery() function's handling of untrusted input and apply stricter dependency constraints.

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

CVE-2022-37601 is a prototype pollution vulnerability in Node.js's loader-utils package that allows attackers to modify object prototypes through the parseQuery() function when processing untrusted input. The vulnerability was fixed by upgrading loader-utils from version 1.4.0 to 1.4.1 and from 2.0.2 to 2.0.4, which implement input validation in the query parsing logic and tighten dependency version constraints to prevent cascading vulnerabilities.

Vulnerability at a Glance

cweCWE-1321 (Improperly Controlled Modification of Object Prototype Attributes)
fixUpgrade loader-utils to versions 1.4.1+ or 2.0.4+ and constrain transitive dependencies with caret ranges
riskAttackers can corrupt object prototypes, potentially leading to code execution, privilege escalation, or denial of service
languageJavaScript/Node.js
root causeInsufficient input validation in loader-utils' parseQuery.js when processing query parameters
vulnerabilityPrototype Pollution in parseQuery()

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:

  1. Bypass security checks: isAdmin, isAuthenticated, or similar boolean flags become true for all objects
  2. Trigger unintended code paths: Configuration-driven logic could be manipulated
  3. Cause denial of service: Polluting critical properties could crash or destabilize the build process
  4. 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.01.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.22.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 modify Object.prototype through 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, and prototype keys 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.lock ensure 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)

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is a JavaScript-specific vulnerability where attackers modify the prototype of built-in objects (like Object.prototype), affecting all objects in the application and potentially enabling code execution or logic bypass.

How do you prevent prototype pollution in Node.js?

Validate and sanitize all untrusted input before processing, use Object.freeze() on critical objects, apply allowlist-based parsing, and keep dependencies updated to patched versions.

What CWE is prototype pollution?

CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) is the primary CWE, often combined with CWE-94 (Improper Control of Generation of Code) when execution is possible.

Is input validation enough to prevent prototype pollution?

Input validation is essential but must be paired with safe parsing libraries and object property checks; avoid merging untrusted objects directly into configuration objects.

Can static analysis detect prototype pollution?

Yes; security scanners like Trivy can detect vulnerable dependency versions, and tools like Semgrep can identify unsafe object merge patterns and unvalidated property assignment.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #10793

Related Articles

critical

How Server-Side Template Injection Happens in EJS and How to Fix It

CVE-2022-29078 is a critical server-side template injection vulnerability in EJS versions prior to 3.1.7 that allows attackers to execute arbitrary code through the `outputFunctionName` parameter. The fix involves upgrading EJS from 2.6.1 to 3.1.7, which implements proper input validation for template rendering options. This vulnerability could allow remote code execution if user-controlled data reaches the template engine without sanitization.

high

How Infinite Loop DoS happens in Node.js ID generation and how to fix it

A critical vulnerability in nanoid versions 3.3.16 and below allowed attackers to trigger infinite loops during random ID generation, causing complete CPU exhaustion and denial of service. The fix upgrades to nanoid 3.3.18, which patches the underlying random number generation flaw that could freeze Node.js applications processing untrusted input.

high

How Denial of Service via Invalid UTF-8 Input Happens in Go and How to Fix It

A high-severity Denial of Service vulnerability in golang.org/x/text (CVE-2026-56852) allowed attackers to crash applications by sending malformed UTF-8 input. The fix involved upgrading the dependency from v0.33.0 to v0.39.0, which tightens UTF-8 validation logic and prevents untrusted input from triggering resource exhaustion. This vulnerability demonstrates why timely dependency updates are critical for maintaining application stability and security.

critical

How missing authorization and code injection happen in Mindustry JavaScript mods and how to fix it

A `TapEvent` handler in `scripts/CommandBlock.js` exposed a full administrative command palette — including a `run-javascript` command that piped player-supplied text straight into `new Function(text)()` — behind nothing more than a team-membership check. Any player who happened to share a team with the block could execute arbitrary JavaScript (and, through Rhino's Java bridge, arbitrary host code) inside the game runtime. The fix adds an explicit `if (!e.player.admin) return;` guard at the top

critical

How URL Injection happens in Node.js template literals and how to fix it

A URL injection vulnerability in `lib/client.js` allowed user-controlled `repo`, `branch`, and `file` parameters to be interpolated directly into fetch URLs without encoding, enabling potential URL manipulation and request hijacking. The fix introduces per-segment percent-encoding via a new `encodePathSegments` helper, neutralizing special characters before they reach the URL construction layer. This closes an exploit primitive that automated attack tooling could chain with other weaknesses.

critical

How a vulnerable websocket-driver dependency happens in Node.js lockfiles and how to fix it

A Trivy scan flagged `websocket-driver@0.7.4` in this repository's `bun.lock` as affected by CVE-2026-54466, a critical issue in a WebSocket protocol handler that parses untrusted HTTP upgrade requests and frame data. The fix upgrades the package to `0.7.5` and adds an explicit `websocket-driver` entry to the lockfile's override block so every transitive consumer — webpack-dev-server, sockjs, faye-websocket — resolves to the patched build instead of the pinned vulnerable one.