TL;DR
- npm supply chain attacks execute through postinstall scripts in packages you install. [email protected] was installed cleanly, with zero vulnerabilities, standard log output, while a transitive dependency had already opened a backdoor, exfiltrated system data, and established persistence.
- A single compromised package version, like event-stream, axios, chalk, reaches every downstream project that runs npm install during the window it is live, without triggering a code review or security alert in any of those projects.
- npm audit lists CVEs but does not rank them by exploitability, does not scan the full transitive dependency tree, and outputs a different JSON schema than gitleaks and grype, so a developer looking for all three must manually cross-reference formats to decide which finding to fix first.
- Opsera Agents’ security-scan orchestrates six tools in one pass, such as gitleaks, semgrep, grype, checkov, hadolint, and npm audit, and returns a single ranked report with exact remediation commands, running against the full resolved package-lock.json, not just direct dependencies.
- This blog covers the 5 npm attack vectors used in npm supply chain attacks, why npm audit and SAST miss the vulnerabilities, a hands-on comparison of manual scanning versus Opsera Agents’ security scan on a repo, and where to add each scan type in your commit, PR, and release workflow.
On a Reddit thread in r/sysadmin, a community of 600k+ sysadmins and developers, engineers are reporting blocked post-install scripts, secret access attempts, and package failures in their dev environments as a routine part of daily work, not as a scheduled audit. Hundreds of npm packages were compromised in a single wave, malicious payloads executing through postinstall hooks that npm install ran without prompting. The question of whether packages that depend on daily updates can still be trusted has no clear answer.
In November 2018, a maintainer of event-stream, with 2 million weekly downloads, transferred ownership to an unknown contributor who published version 3.3.6, adding flatmap-stream as a dependency that did not exist before the transfer. Flatmap-stream carried an encrypted payload that extracted private keys from the Copay Bitcoin wallet and transmitted them to an attacker-controlled server, giving the attacker full access to any Bitcoin held in affected wallets.
The postinstall hook executed inside the normal install log, indistinguishable from any other dependency setup step, with no diff and no new entry in package.json. The attack ran for two months before a developer auditing the flatmap-stream source noticed an encrypted string that did not match the GitHub repository.
The same path was followed in ua-parser-js (2021), node-ipc (2022), and axios (2025). Understanding why requires looking at how these attacks are structured.
What is an npm Supply Chain Attack?
An npm supply chain attack delivers malicious code through a dependency your project installs, not through code your team wrote. For example, when a developer runs npm install locally or a CI/CD pipeline installs dependencies during a build, npm automatically executes postinstall scripts with the same privileges as the install process. Because PR review and SAST typically analyze application source code rather than the contents inside node_modules, malicious dependency code can go unnoticed.
The March 2026 Axios compromise, documented by Socket’s research team, shows exactly how this plays out. [email protected] pulled in [email protected], a package published minutes earlier and confirmed malicious by Socket, which deployed a remote access trojan that executed arbitrary commands, exfiltrated system data, and established persistence on the infected machine:
| { “name”: “plain-crypto-js”, “version”: “4.2.1”, “scripts”: { “postinstall”: “node install.js” } } |
And on running:
| npm install axios |
It installed cleanly:
| added 1 package, and audited 523 packages in 2s found 0 vulnerabilities |
npm reported zero vulnerabilities. The remote access trojan had already executed install.js, opened a backdoor, and begun exfiltrating data. Nothing in the install output indicated anything had run.
5 Ways Attackers Inject Malicious Code Into Your npm Dependencies
Each vector exploits a different gap in how npm resolves, installs, and trusts packages, and none require touching your application code, which is why standard code review and SAST miss all five.
1. Dependency Confusion: Public Registry Overrides Your Private Package
A team depends on an internal npm package used across multiple services. An attacker publishes a public package with the same name and a higher version number. When a developer’s machine or CI/CD pipeline runs npm install, the package manager pulls the attacker-controlled package instead of the internal one. The build succeeds normally, but the malicious package executes a hidden postinstall script during installation, giving the attacker access to secrets and tokens before the application code ever runs.
2. Typosquatting: One Miskeyed Character Installs the Attacker’s Package
An attacker registers crossenv on npm, one transposed character from the legitimate cross-env, which is enough for npm’s resolver to install the attacker’s version when a developer miskeys the package name. The malicious version collected over 700 downloads before npm removed it.
3. Malicious postinstall Scripts: Arbitrary Code Runs Before Your App Does
A package’s package.json includes a postinstall field pointing to a shell script. When npm install completes, npm executes that script with the privileges of the process running the install. The script can read environment variables, write files, open network connections, and exfiltrate credentials, and npm’s terminal output shows nothing but the standard package resolution log.
4. Compromised Maintainer Accounts: Malicious Version Ships Under a Trusted Name
When a maintainer account is phished, or ownership is transferred to an unknown contributor, the attacker publishes a new version through npm’s normal release pipeline, authenticated, versioned, and indistinguishable from a legitimate update. The package passes npm audit because no CVE has been filed yet and appears unchanged in any diff; the malicious version ships under the same package name and semver range the project already trusts, inheriting the package’s download count, GitHub stars, and existing semver pin in package.json. Nothing in the install flow signals that the maintainer changed.
5. Lockfile Drift: A Different Package Version Resolves With No Diff and No Flag
package-lock.json stores the resolved version and integrity hash of every installed package. If the lockfile is not committed, regenerated inconsistently, or bypassed with the –no-package-lock option, a different version of a package is resolved on the next install. If that version introduced a vulnerability after the last locked install, it enters the build with no diff, no PR, and no flag. The discrepancy is invisible unless a tool like grype re-resolves the full dependency tree and compares installed package hashes against the lockfile on every build.
Why npm audit Misses Malicious npm Packages
npm audit cross-references your installed packages against a database of known CVEs. It is a useful first layer. It is not sufficient as a standalone security gate for four specific reasons.
- CVE severity does not map to exploitability in your codebase. npm audit marks a vulnerability as critical based on its CVSS score in the database. A critical vulnerability in a package that is imported only in a dead code path carries a different real-world risk than the same vulnerability in a package called on every API request. npm audit does not distinguish between the two. Both appear critical in the output.
- Transitive dependency scanning is incomplete. npm audit checks direct dependencies against the advisory database. It does not perform a deep scan of every resolved package in your full dependency tree using a vulnerability database, as other security scanning queries do. A compromised transitive package two levels down may not flag in npm audit output at all.
- Running gitleaks, semgrep, and grype separately produces three reports in three formats with no unified risk ranking. A team running all three gets a gitleaks JSON report, a semgrep SARIF output, and a grype table, all of which require an individual call to identify which findings to address first, in what order, and what the combined risk score actually is.
- Teams that scan only at release find vulnerabilities weeks after the dependency is entered into the codebase. A developer adds a package on a Tuesday. It passes PR review, which checks code logic and does not inspect package versions. It passes SAST, which checks code patterns and does not check the dependency tree. It lands in the main. A scheduled security scan flags the CVE two weeks later. By then, the package is in three feature branches and two staging deployments.
How Opsera Agents’ Security-Scan Works
Teams can continue using tools like Gitleaks, Semgrep, and Grype as part of their existing workflows. Opsera Agents does not require replacing native security tooling; instead, it unifies findings across the stack, identifies coverage gaps such as unscanned transitive dependencies, and automatically introduces additional scanners only where protection is missing.
Connecting to Your IDE
Opsera Agents connect to your development environment through the Model Context Protocol (MCP). The source code never leaves your machine. Analysis runs locally; only structured findings are returned to the Opsera portal.
The agent works inside Claude Code, Cursor, and VS Code. Setup is a single MCP configuration line, no CLI flags, no config files to maintain:
| claude mcp add –scope user –transport http opsera https://agent.opsera.ai/mcp |
Before running a scan, the agent asks five plain-English questions: which directory to scan, which scan types to run, which severity threshold to apply, whether to include container scanning, and whether to report new findings only or the full backlog. No YAML. No flag lookup.
What Opsera Runs Under the Hood
A single security-scan call orchestrates six tools in sequence:
| Tool | What it scans |
| gitleaks | Hardcoded secrets, API keys, credentials, and tokens in source files and git history |
| semgrep | OWASP Top 10 vulnerabilities in application code, such as SQL injection, XSS, and insecure authentication patterns |
| grype | CVEs across the full resolved dependency tree, including transitive packages |
| checkov | Infrastructure-as-code misconfigurations in Terraform, CloudFormation, and Kubernetes manifests |
| hadolint | Dockerfile security linting, insecure base images, missing USER directives, risky RUN commands |
| npm audit | Known vulnerabilities in direct dependencies via the npm advisory database |
If any of these tools are not installed on the machine, the agent installs them automatically before running. No manual setup. No “tool not found” errors that silently skip a scan category.
What Makes This Different From Running Security Extensions Separately
Auto-installation of missing tools mid-run. A developer who does not have grype installed does not get a partial scan with a silent gap in coverage. The agent detects the missing tool, installs it, and continues. The scan report reflects actual coverage, not assumed coverage.
Full transitive dependency scanning via grype. The scan runs against package-lock.json with full scope, resolving every package in the dependency tree, not just the packages listed directly in package.json. A vulnerable package three levels deep in the transitive chain surfaces in the same report as a vulnerability in a direct dependency.
A single-ranked report, not three separate tool outputs. Findings from all six tools are returned as a single unified report with a risk score from 0 to 100, findings grouped by severity, and exact remediation commands, for example, npm install [email protected], directly in the output. Now, let’s go through a pre-commit scan and see how Opsera Agents do a security scan for uncommitted changes
Running the Pre-Commit Scan in Opsera Agents
The pre-commit scan in Opsera Agents runs in diff-aware new-findings mode:
Now, as shown in the snapshot below, running the prompt:
| Run a security scan and only show me critical and high issues new from uncommitted changes. |
The workflow starts with the agent discovering which DevSecOps agents are enabled for the tenant. In the Orderflow environment, Opsera exposed five active agents: security-scan, compliance-audit, github-waf-compliance, architecture-analyze, and dora-metrics.
The scan was then configured directly from the terminal in pre-commit mode against the Orderflow repository:
- Repository path: /Users/shivanshmishra/Desktop/Orderflow
- Scan type: full
- Engines enabled:
- Semgrep (SAST)
- Grype (dependency CVEs)
- Checkov (IaC)
- Hadolint (container linting)
- Secrets detection
- Package leakage scanning
The important detail is the execution mode. Instead of treating every vulnerability in the repository as equally relevant, the agent classified findings into:
| Classification | Meaning |
| NEW | Introduced by the developer’s current uncommitted changes |
| EXISTING | Already present in the committed code before the current work |
That classification changes how developers interact with security tooling. Instead of inheriting years of historical noise from the backlog with every commit, the scan answers a much narrower operational question: “What security risk did this specific changeset introduce?”
The generated report below shows the exact result from the Orderflow scan.
Result
The scan still flagged two high-severity findings from Semgrep:
- html.security.audit.missing-integrity.missing-integrity
- security-report-Orderflow-20260506.html:208
- html.security.audit.missing-integrity.missing-integrity
- security-report-Orderflow-20260506.html:211
But both findings were classified as EXISTING, not introduced by the current diff.
The remediation guidance was explicit: Add a Subresource Integrity (integrity) attribute and crossorigin, typically, for externally hosted scripts/styles, or host the assets locally.
That distinction is operationally important. A developer working on unrelated functionality should not be blocked by historical findings they did not introduce. The scan preserves visibility into existing risk while isolating accountability for newly introduced vulnerabilities.
Without diff-aware scanning, teams quickly hit alert fatigue. Every commit surfaces the same inherited vulnerabilities repeatedly, developers stop reading scan output, and real regressions blend into the noise.
With pre-commit classification enabled, the workflow becomes actionable:
- The developer adds a package or modifies code
- Opsera scans only the changed surface area
- Critical/high findings introduced by the diff are isolated
- Existing technical debt is separated from new risk
- The developer fixes issues before the code ever reaches CI
That moves vulnerability remediation from post-merge cleanup into the developer workflow itself, where fixes are cheapest, fastest, and least disruptive.
Conclusion
The handlebars vulnerability was publicly disclosed in 2019. It was still in Orderflow in 2025, six years later, because no automated scan had ever run against the project’s fully resolved dependency tree. The SAST scan returned zero findings. The dependency scan returned a critical.
At 2 million packages in the npm registry, with transitive dependency chains stretching three to five levels deep in any production application, manual dependency review does not scale. A single security-scan call through Opsera Agents orchestrates six tools, auto-installs any missing tooling, scans the full resolved tree, and returns one ranked report with exact remediation commands.
The handlebars fix is npm install [email protected]. The scan finds it in under five minutes. The pre-commit gate means the next developer who adds a package with a known CVE finds out before the push, not two weeks later in a staging environment.
Try it out for free (unlimited use for small teams) at opsera.ai/agents. Runs inside your IDE and Claude Code with one command.