Back to Blog
high SEVERITY5 min read

How Dependency Version Pinning Prevents Supply Chain Attacks in Node.js and How to Fix It

A critical supply chain vulnerability in `package.json` allowed automatic updates to a cryptographic library with known weaknesses. By pinning `rijndael-js` to version `2.0.0` instead of allowing `^2.0.0` updates, the fix prevents silent installation of vulnerable versions that could expose downstream consumers to weak block cipher modes and authentication bypasses.

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

Answer Summary

This is a **Dependency Confusion / Supply Chain Vulnerability** (CWE-1104) in Node.js where the `^2.0.0` semver range for `rijndael-js` in `package.json:29` allowed automatic patch/minor updates that could silently introduce vulnerable versions. The fix pins the dependency to exactly `2.0.0`, removing the caret operator to prevent automatic updates to potentially compromised or weakened cryptographic implementations. This protects downstream consumers from man-in-the-middle attacks exploiting weak ECB mode and improper IV handling in the rijndael-js library.

Vulnerability at a Glance

cweCWE-1104 (Uncontrolled Search Path Element), CWE-1395 (Dependency on Vulnerable Third-Party Component)
fixPin rijndael-js to exact version 2.0.0 by removing ^ prefix
riskAutomatic updates to vulnerable cryptographic library exposing MITM attacks, traffic decryption, and command modification
languageJavaScript/Node.js
root causeCaret (^) semver operator allowed automatic patch/minor version updates without security review
vulnerabilitySupply Chain / Dependency Confusion

How Dependency Version Pinning Prevents Supply Chain Attacks in Node.js and How to Fix It

Option C: Context-First Introduction

The package.json file in this Node.js repository manages cryptographic operations through the rijndael-js library, but a flaw in the dependency specification on line 29 created a supply chain security risk. The entry "rijndael-js": "^2.0.0" used a caret-prefixed semantic version range that automatically allows patch and minor version updates—potentially pulling in compromised or weakened versions of this encryption library without explicit review or testing.

This matters for developers because cryptographic libraries require rigorous vetting. The rijndael-js package implements Rijndael (AES) encryption, and version 2.0.0 has known limitations: it uses weak block cipher modes (likely ECB) and lacks authenticated encryption. When the ^ operator allows automatic updates, downstream consumers of this package could silently receive versions with even worse security properties—or worse, a compromised version from a compromised maintainer account.

The Vulnerability Explained

The vulnerable dependency specification:

// package.json (line 29) - BEFORE
"dependencies": {
  "@iobroker/adapter-core": "^3.4.3",
  "crc-32": "^1.2.0",
  "rijndael-js": "^2.0.0"
}

The specific problem: The caret (^) in ^2.0.0 tells npm to accept any version from 2.0.0 up to (but not including) 3.0.0. This means npm install could automatically install 2.0.1, 2.1.0, or any future 2.x.x release—versions that may:
- Contain intentional backdoors from compromised maintainer accounts
- Introduce breaking cryptographic changes (weaker modes, broken IV handling)
- Include supply chain malware injected during the build process

The real-world impact: This is an ioBroker adapter—a home automation platform where this package handles device communication encryption. An attacker in a man-in-the-middle position could exploit weak or broken encryption to:
1. Decrypt traffic between smart home devices and controllers
2. Modify commands undetected (lacking authentication)
3. Potentially inject malicious firmware updates or device commands

The rijndael-js library specifically has documented weaknesses in IV handling and lacks authenticated encryption (AEAD). Automatic updates could worsen these issues or introduce new vulnerabilities without consumers knowing their encryption had degraded.

The Fix

The hardened dependency specification:

// package.json:29 - AFTER
"dependencies": {
  "@iobroker/adapter-core": "^3.4.3",
  "crc-32": "^1.2.0",
-    "rijndael-js": "^2.0.0"
+    "rijndael-js": "2.0.0"
}

What changed:
- Removed the ^ caret prefix from rijndael-js
- Pinned to exact version 2.0.0 with no automatic update allowance

Why this solves the problem:
Exact version pinning ("2.0.0" vs "^2.0.0") ensures that every install gets precisely the same code that was security-reviewed. Updates require explicit human intervention—forcing a conscious security decision when any version change occurs.

This is particularly critical for cryptographic dependencies where:
- Subtle implementation changes break security guarantees
- Supply chain attacks on crypto libraries have high impact
- Downstream consumers depend on specific, verified security properties

Prevention & Best Practices

For Dependency Management

Practice Implementation
Exact version pinning Use "pkg": "1.2.3" not "^1.2.3" for security-critical deps
Lock file verification Ensure package-lock.json is committed and reviewed
Dependency auditing Run npm audit in CI/CD pipelines
Cryptographic library vetting Prefer well-audited libs (crypto, noble-ciphers) over custom implementations

Security Standards

  • OWASP Dependency-Check: Automated detection of vulnerable components
  • CWE-1104: Uncontrolled Search Path Element—covers dependency resolution risks
  • CWE-1395: Dependency on Vulnerable Third-Party Component
  • SLSA (Supply Chain Levels for Software Artifacts): Framework for securing build pipelines

Detection Tools

# Audit installed dependencies
npm audit

# Check for outdated packages
npm outdated

# Verify lock file integrity
npm ci  # fails if package.json and lock file mismatch

Semgrep rule for vulnerable semver patterns:
https://semgrep.dev/r?q=javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp

Key Takeaways

  • Never use ^ or ~ for cryptographic dependencies—the rijndael-js entry in package.json:29 now uses exact version pinning to prevent automatic updates to unvetted versions
  • The ^2.0.02.0.0 change blocks CWE-1104 exploitation by removing automatic version resolution that could introduce supply chain compromises
  • Downstream ioBroker consumers are now protected from silently receiving weakened or compromised encryption implementations
  • Explicit updates required—any future rijndael-js upgrade must be intentional, with security review of the new version's cryptographic properties
  • Consider replacing rijndael-js—Node.js built-in crypto module or noble-ciphers provide authenticated encryption that this library lacks

How Orbis AppSec Detected This

Source: The package.json dependency manifest where "rijndael-js": "^2.0.0" allows automatic version resolution to any 2.x.x release.

Sink: Downstream consumption in cryptographic operations where the library's weak ECB mode and lack of authenticated encryption create exploitable conditions for MITM attackers.

Missing control: No version pinning or integrity verification (no package-lock.json enforcement, no explicit security review of automatic updates).

CWE: CWE-1104: Uncontrolled Search Path Element and CWE-1395: Dependency on Vulnerable Third-Party Component

Fix: Removed the ^ semver operator to pin rijndael-js to exactly version 2.0.0, preventing automatic updates to potentially vulnerable future versions.

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 security requires vigilance at every dependency declaration. The simple change from "^2.0.0" to "2.0.0" in package.json represents a fundamental security principle: trust must be explicit, not automatic. For cryptographic libraries especially, where the cost of a compromised dependency is total security failure, exact version pinning is non-negotiable.

Review your package.json files today. Identify security-critical dependencies—encryption, authentication, network communication—and pin them to exact versions. Your downstream consumers depend on it.

References

Frequently Asked Questions

What is this supply chain vulnerability?

It's a dependency management flaw where the ^ semver operator in package.json automatically updates rijndael-js to newer versions that may contain cryptographic weaknesses, exposing downstream consumers to attacks without their knowledge.

How do you prevent supply chain vulnerabilities in Node.js?

Pin exact versions in package.json using "dependency": "X.Y.Z" instead of "^X.Y.Z" or "~X.Y.Z", audit dependencies with npm audit, use lock files, and verify cryptographic libraries for authenticated encryption support.

What CWE is dependency version pinning?

CWE-1104 (Uncontrolled Search Path Element) and CWE-1395 (Dependency on Vulnerable Third-Party Component) cover risks from uncontrolled dependency updates.

Is using package-lock.json enough to prevent this?

No—package-lock.json helps but doesn't prevent fresh installs from resolving to newly published vulnerable versions. Exact version pinning in package.json is the first line of defense.

Can static analysis detect vulnerable dependency versions?

Yes—tools like npm audit, Snyk, and Semgrep can flag outdated or vulnerable dependencies, but they may miss newly published vulnerable versions before they're indexed.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #358

Related Articles

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.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) in the `tar` npm package allowed attackers to craft malicious gzip archives that could exhaust memory or CPU during decompression. The fix upgrades `tar` from 7.5.11 to 7.5.21 across `package.json` and `package-lock.json`, closing the resource-exhaustion path without changing any application code.

high

How Quadratic Complexity Denial of Service happens in JavaScript YAML parsing and how to fix it

A high-severity dependency vulnerability in `js-yaml`, used transitively by `electron-builder`/`electron-updater` inside the DSA Desktop app, allowed attacker-controlled YAML with `!!omap` tags to trigger quadratic-time CPU consumption during parsing. The fix pins `js-yaml` to the patched `4.3.1` (and `3.15.1` for the legacy branch) release via both `package.json` overrides and `package-lock.json`.

high

How mutable GitHub Actions tags enable supply-chain attacks and how to fix them

A Node.js library's composite GitHub Action was using mutable version tags (`@v6`, `@v4.36.3`) for action dependencies, creating a supply-chain attack vector. The fix pins both `actions/setup-node` and `github/codeql-action/upload-sarif` to specific 40-character commit SHAs, eliminating the risk of silent repointing attacks.

high

How Dependabot missing cooldown periods happens in GitHub Actions and how to fix it

The repository's `.github/dependabot.yml` had no `cooldown` block, meaning Dependabot could open PRs to adopt a package version the moment it was published — before the ecosystem had any chance to flag it as malicious or broken. The fix adds a `cooldown.default-days: 7` setting to each `package-ecosystem` entry, forcing a one-week buffer before new releases are proposed.

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.