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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #10793

Related Articles

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun

high

installPlugin(): Unvalidated npm Package Names Reach npm install

A plugin manager service exposed an `installPlugin(plugin: PluginInfo)` method that passed `plugin.packageName` and `plugin.version` straight into the platform's npm install routine with no validation, no blocklist, and no integrity verification of the fetched tarball. Because npm treats a non-semver "version" as a fetch specifier — a tarball URL, a git ref, a local path — an attacker who could influence the plugin listing could get arbitrary code installed and executed with full Electron/Node p

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.