In early 2023, security researchers identified a wave of malicious npm packages designed to impersonate ESLint — one of the most widely used JavaScript linting tools with over 30 million weekly downloads. The attack leveraged typosquatting and namespace confusion to trick developers into installing packages that exfiltrated credentials, environment variables, and SSH keys.
The Attack Pattern
The malicious packages followed a well-established playbook with some clever twists. Attackers published packages with names closely resembling legitimate ESLint packages and plugins:
- Variations on
eslint-config-*patterns - Typosquats of popular ESLint plugins
- Packages mimicking ESLint's official scope
When installed, these packages executed postinstall scripts that:
- Collected environment variables (including CI/CD tokens and cloud credentials)
- Read
.npmrcfiles to extract npm authentication tokens - Scanned for SSH keys in
~/.ssh/ - Exfiltrated all collected data to attacker-controlled servers
The attack was particularly effective because ESLint configurations are typically installed early in a project's setup, often before security tooling is in place.
Why ESLint Was a Perfect Target
ESLint's ecosystem is massive and fragmented. There are thousands of ESLint plugins and configurations published on npm. Developers regularly search for and install ESLint packages from unfamiliar publishers. This creates an environment where malicious packages can hide in plain sight.
The Plugin Ecosystem Problem
A typical JavaScript project might use 5-10 ESLint-related packages:
{
"devDependencies": {
"eslint": "^8.33.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-jsx-a11y": "^6.7.1",
"@typescript-eslint/parser": "^5.49.0",
"@typescript-eslint/eslint-plugin": "^5.49.0"
}
}
Each of these is a separate package from a separate publisher. Developers are accustomed to installing ESLint packages from various authors, so a malicious package from an unknown publisher doesn't raise the same red flags it might for other types of packages.
The devDependency Blind Spot
ESLint packages are always installed as devDependencies, which creates a psychological blind spot. Many developers and security teams focus their supply chain security efforts on production dependencies, assuming devDependencies are lower risk. But devDependencies:
- Execute postinstall scripts with full system access
- Run in CI/CD environments with access to secrets
- Execute on developer machines with access to SSH keys, cloud credentials, and source code
A compromised devDependency has the same access to your system as a compromised production dependency.
The Postinstall Script Vector
The malicious packages relied on npm's postinstall script feature — a script that runs automatically after a package is installed. This feature exists for legitimate purposes (compiling native modules, for example), but it's also the most common vector for supply chain attacks on npm.
{
"scripts": {
"postinstall": "node setup.js"
}
}
The setup.js file in these malicious packages was obfuscated but performed straightforward credential theft:
// Simplified representation of the malicious behavior
const env = process.env;
const npmrc = readFileSync(join(homedir(), '.npmrc'), 'utf8');
const sshKeys = readdirSync(join(homedir(), '.ssh'));
// Exfiltrate to attacker server
Detection and Response
The packages were identified through a combination of automated scanning and researcher reports. npm removed them, but the window between publication and removal — typically hours to days — is long enough for significant damage, especially for packages that get installed in CI/CD pipelines running continuously.
Detection Challenges
- Obfuscation: The malicious code was obfuscated to avoid pattern-matching detection
- Delayed execution: Some packages only activated their malicious behavior after a delay or only in CI/CD environments
- Minimal footprint: The exfiltration was done via HTTPS to blend in with normal network traffic
- Legitimate-looking code: The packages included actual ESLint configurations to appear functional
The npm Ecosystem's Structural Vulnerabilities
This attack exploited several structural features of the npm ecosystem:
1. No Namespace Reservation
Anyone can publish a package with a name that looks like it belongs to the ESLint project. Unlike scoped packages (@eslint/config), unscoped packages in the eslint-* namespace are first-come, first-served.
2. Postinstall Scripts Run By Default
npm executes postinstall scripts automatically unless users explicitly opt out with --ignore-scripts. Most developers don't use this flag because it breaks legitimate packages that need compilation.
3. Limited Publisher Verification
npm doesn't verify that the publisher of an eslint-plugin-something package has any relationship with the ESLint project. The package name implies affiliation where none exists.
4. Rapid Publication Without Review
Anyone can publish a package to npm instantly. There's no review process, no waiting period, and no approval needed. This is great for the open-source ecosystem's velocity, but it means malicious packages are live from the moment they're published.
Protecting Your Organization
Disable Postinstall Scripts Where Possible
Consider running npm install --ignore-scripts and then explicitly building packages that need it. Tools like npx can-i-ignore-scripts can help identify which packages actually need postinstall scripts.
Use Lockfiles Religiously
package-lock.json ensures you install the exact versions that were tested. Don't delete it, don't ignore it, and always commit it.
Audit Before Installing
Run npm audit on new dependencies before adding them. Check the package's age, download count, publisher history, and GitHub repository.
Monitor for Typosquats
Be extremely careful when copying package names from blog posts, ChatGPT responses, or search results. Always verify the exact package name against the official documentation.
How Safeguard.sh Helps
Safeguard.sh provides multiple layers of defense against this type of supply chain attack:
- Malicious Package Detection: Safeguard.sh analyzes packages for known malicious patterns, including obfuscated postinstall scripts, credential harvesting behavior, and data exfiltration techniques.
- Dependency Risk Scoring: Every package in your dependency tree receives a risk score based on publisher history, age, download patterns, and behavioral analysis, helping you identify suspicious packages before they're installed.
- SBOM Integrity Monitoring: Safeguard.sh tracks your SBOM over time, alerting you when new, unvetted dependencies appear — especially devDependencies that are often overlooked.
- Typosquat Detection: Safeguard.sh identifies packages with names suspiciously similar to popular packages, flagging potential typosquats before they enter your supply chain.
The ESLint incident is a reminder that supply chain attacks don't require sophisticated exploits. They require exploiting the trust and habits that developers have built up around everyday tools. Every package install is a trust decision, and tooling like Safeguard.sh helps make those decisions informed ones.