New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket
Blog
Security News

The Hidden Blast Radius of the Axios Compromise

The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Ahmad Nassri

April 1, 2026

13 min read

The Hidden Blast Radius of the Axios Compromise
Sidebar CTA Background

Secure your dependencies with us

Socket proactively blocks malicious open source packages in your code.
Install

Yesterday, we reported on a supply chain attack targeting Axios that introduced a malicious dependency (plain-crypto-js) into specific npm releases.

At first glance, the scope seemed contained:

  • Two compromised Axios versions
  • A short exposure window
  • A malicious dependency that was quickly removed

Over the past 24 hours, we’re seeing many teams focus on checking their lockfiles and node_modules directories, but that only captures part of the picture, especially when tools are executed dynamically via npx.

During the exposure window, widely used tools, including CI systems, developer CLIs, build tools like Nx, and even MCP servers, could resolve the compromised version through normal dependency ranges, often without explicitly depending on Axios at all.

This incident is one of the clearest examples of dynamics that we have been warning about for years. Modern dependency resolution makes incidents like this far harder to reason about and far broader in impact than they initially appear.

This post explains how that happens, where common assumptions break down (especially around lockfiles and npx), and why the blast radius is often larger than it looks.

The Part That’s Easy to Understand#

A malicious version of Axios (1.14.1) was published to npm. That version introduced a new dependency (plain-crypto-js@4.2.1) containing a multi-stage malware payload.

Any project installing Axios during that window could pull the malicious version. If Axios was already resolved in your lockfile and installs respected that lockfile, you were likely protected. That’s where most explanations stop. During this attack, we have observed common workflows where this assumption does not hold, particularly when tools are executed dynamically via npx.

The Part That’s Not#

What’s much harder to understand is how many systems could have installed that version without ever explicitly depending on Axios.

This comes down to one detail:

Most packages do not pin exact dependency versions.

Instead, they use version ranges like:

axios: "^1.13.5"

That range means:

  • Accept anything ≥ 1.13.5 and < 2.0.0
  • Always resolve to the latest matching version at install time

When axios@1.14.1 was published, it became the default resolution for that range, without any code changes or alerts. It was just freshly installed.

This Was Not Isolated to One Package#

It’s easy to focus on a single example, but the pattern is widespread.

The key condition for exposure was simple:

  • A package declares an Axios version range that includes 1.14.1
  • A fresh install or execution happens while that version is live
  • No lockfile (or no applicable lockfile) constrains resolution
  • A non-deterministic install occurs (e.g. npm install without a lockfile, or installing a CLI outside a project context)

Under those conditions, the package manager will select the malicious version by default.

During the exposure window, a large number of widely used tools met these conditions.

Note: None of the packages listed below were compromised, and their dependency declarations are typical and appropriate for the npm ecosystem.
Using semver ranges is a deliberate design tradeoff that enables compatibility and deduplication across the dependency graph. These examples illustrate how that same mechanism can expand the blast radius of a short-lived malicious release.

The examples below are not exhaustive. They illustrate how common this pattern is across CI tooling, CLIs, and frameworks:

  • CI tooling (@datadog/datadog-ci)
    At the time of the attack it declared axios: "^1.13.5" across multiple sub-packages, such as @datadog/datadog-ci-plugin-coverage.
    Any fresh install or npx execution during the window resolves to 1.14.1.
  • Observability / instrumentation (e.g. Honeycomb OpenTelemetry tooling)
    Uses ranges like ^1.1.3 for Axios.
    Often installed dynamically in CI or runtime environments without lockfile protection.
  • Contract testing frameworks (Pact)
    Declares Axios with a compatible range (^1.12.2).
    Commonly installed in CI pipelines where fresh installs are routine.
  • Developer CLIs (@aws-amplify/cli, others)
    Frequently run via npx or installed globally, declares Axios range (^1.11.0)
    This triggers real-time dependency resolution against the registry rather than a project lockfile.
  • Static site tooling (Gatsby)
    Declares a broad Axios range (^1.6.4).
    New project scaffolding (npx gatsby new) or fresh installs would resolve to the latest matching version.
  • Build systems (Nx)
    Depend on Axios transitively with ranges like ^1.2.0.
    Any environment rebuilding dependencies without a pinned lockfile could pull in the compromised version.
  • CI utilities (wait-on)
    Directly depends on Axios with ^1.13.5.
    Commonly used in ephemeral CI jobs where dependencies are installed from scratch.

In all of these cases:

  • The package itself was not compromised
  • The dependency declaration was valid and typical
  • The risk was introduced entirely at resolution time

In one observable case, a CI pipeline running a CLI via npx pulled in the malicious dependency through a transitive Axios range, resulting in observable command-and-control traffic during a build step.

This is what makes the blast radius unintuitive.

The question is not:

“Do you depend on Axios?”

It is:

“Did anything you executed resolve Axios during that window?”

Additional Exposure From MCP Tooling#

Looking beyond traditional CI and CLI tooling, similar patterns show up in MCP servers and agent-oriented packages.

In a sample of MCP servers from a public leaderboard, a significant portion included Axios ranges that would have resolved to the compromised version during the exposure window.

A few examples:

  • task-master-ai@0.43.1
    Declares Axios transitively via pipenet@1.4.0 with range ^1.7.3
  • n8n@2.14.2
    Pins Axios directly to 1.13.5, but multiple transitive dependencies — including @1password/connect, ibm-cloud-sdk-core, snowflake-sdk, and others — use ranges such as ^1.10.0, ^1.13.5, and ^1.6.2, which would resolve to 1.14.1 during the attack window
  • exa-mcp-server@3.2.0
    Uses Axios with range ^1.13.6, both directly and via agnost@0.1.10
  • claude-flow@3.5.48
    Pulls Axios via agentic-flow@2.0.7 and pipenet@1.4.0, with range ^1.12.2
  • firecrawl-mcp@3.11.0
    Depends on Axios via @mendable/firecrawl-js@4.15.2 with range ^1.13.5
  • mcp-atlassian@2.1.0
    Declares Axios directly with range ^1.11.0

Exposure Across Widely Used Production SDKs#

To understand how far this pattern extends, we looked at a sample of widely used SDKs and infrastructure packages commonly found in production environments.

They are core integrations used across CI systems, backend services, and developer workflows. Across this sample, the same pattern holds: broad semver ranges and transitive usage.

A few examples:

Why the Blast Radius Is Larger Than It Looks#

While this post focuses on the Axios compromise, this pattern is not unique. Recent incidents, including the hijacking of the widely used is package and maintainer compromises affecting packages in the eslint and prettier ecosystems, in 2025, have shown how malicious versions can be introduced and propagate quickly through dependency graphs. Any widely used package with broad semver ranges and transitive adoption can exhibit the same behavior under the right conditions.

There are three compounding factors that make incidents like this difficult to scope.

Version Ranges Are the Default

Most packages intentionally avoid pinning dependencies. This is intentional. Pinning dependencies in published packages would force every consumer to install that exact version, leading to duplication, and version conflicts across the dependency tree.

Using semver ranges allows package managers to share compatible versions across dependencies, reducing install size and avoiding conflicts. This is a deliberate ecosystem tradeoff, not negligence.

This keeps ecosystems flexible and avoids and bloat, but it also means dependency graphs are constantly shifting based on what is available in the registry.

Lockfiles Only Work Sometimes

Lockfiles are often presented as the solution, but they only protect you under specific conditions:

  • You have a lockfile
  • You use install modes that respect it
  • You are not introducing new dependencies

Many real-world workflows fall outside those conditions:

  • Running tools via npx or global installs, especially in CI environments
  • Fresh environments or ephemeral builds
  • Projects without committed lockfiles

Even when a lockfile exists, it does not apply to everything you execute.

Darcy Clarke, founder of vlt and former npm Engineering Manager of Community & Open Source, explained it this way:

When you're installing or executing something new, the dependency graph has to be recalculated. That’s how package managers work. Lockfiles don’t prevent net-new installs when updating/adding new dependencies. That’s the point.

vlt takes the approach that all third-party packages are untrusted & gates the execution of lifecycle scripts with the use of Socket's insights. The minute Socket flagged the malicious versions of Axios & plain-crypto-js - if you were using vlt exec-local - you were protected from this exploit.

Lockfiles make existing installs deterministic. They do not make new installs safe.

In most cases, well-configured CI workflows that rely on committed lockfiles and deterministic installs (e.g. npm ci) are not affected by this class of issue.

However, this protection breaks down when new dependency resolution is introduced, such as when adding or updating dependencies, executing tools dynamically, or automatically merging dependency update PRs.

This dynamic is amplified in environments where dependency updates are automated, including with bots or AI-driven workflows, where new dependency resolution can be introduced continuously and without direct human review.

Execution Patterns Like npx Change the Model

CI install workflows are generally safe, but using npx in CI is not necessarily safe. This introduces another layer of complexity.

These tools are often:

  • Not part of your project dependencies
  • Not represented in your lockfile
  • Installed on demand at execution time

When using npx, a locally installed version will be used if available. Otherwise, the package is fetched from the registry and its dependencies are resolved at execution time, which can introduce risk if a malicious version is briefly available.

That means every execution can trigger fresh dependency resolution against the current state of the registry. You are essentially trusting whatever versions exist at that exact moment.

Even explicitly pinning a version at execution time does not fully solve this.

For example, running npx foo@1.2.3 ensures that specific version of foo is used, but its dependencies are still resolved dynamically based on their declared version ranges. Those transitive dependencies are not pinned and will be resolved against whatever is available in the registry at that moment.

This is compounded by the fact that npm does not distribute lockfiles with published packages. Lockfiles are intentionally excluded from registry artifacts (locking transitive dependencies in a published package would cause version conflicts across the wider dependency tree and prevent deduplication), which means there is no way for package authors to enforce a fully pinned dependency graph for consumers at install time.

The Hardest Part of Figuring Out If You Were Affected#

This is where things become genuinely difficult. After the malicious version is removed:

  • npm no longer serves it
  • dependency trees resolve differently
  • reinstalling produces a clean result

So you might check your project today and see nothing unusual. That does not mean you were not exposed.

Reconstructing what happened during the window would require a complete snapshot of the ecosystem at that point in time, which most environments do not retain. It requires:

  • Full lockfiles from that exact point in time
  • Complete build logs showing resolved versions
  • Artifact snapshots of node_modules
  • Network telemetry capturing outbound requests

Most environments do not retain all of this. And even when they do, it may not be complete enough to answer definitively.

There are additional complications:

  • You may see the malicious dependency (plain-crypto-js) without ever seeing Axios itself.
  • Dependencies may be vendored or bundled inside published packages, meaning the malicious code is not visible as a direct dependency and may not appear in standard dependency trees.
  • You may have executed it transiently in CI without persisting it anywhere
  • You cannot re-run installs to reproduce the state, because the registry has already changed.
  • npm does not retain a public, queryable history of all dependency graphs as they existed at a point in time.

Even with perfect logs, you are often reconstructing behavior indirectly. In many cases, the best you can do is infer exposure based on timing and partial evidence.

In other words:

The absence of evidence after the fact is not strong evidence of absence.

The Core Problem: Time-Dependent Dependency Resolution#

At the center of this is a fundamental property of the ecosystem:

Dependency resolution is time-dependent.

Two identical commands run hours apart can produce different results:

  • Before the attack → safe dependency tree
  • During the attack → compromised dependency tree
  • After removal → safe again

Nothing in your code changed. The only thing that changed is the registry.

What Actually Helps (and What Doesn’t Fully Solve It)#

There is no clean, universally accepted solution here. While there are some mitigations that reduce risk in scenarios like this, none of them fully eliminate it.

  • Lockfiles
    Provide strong protection for existing dependencies, but do not apply to new installs, npx executions, or dynamic tooling in CI.
  • Pinned dependencies
    Reduce exposure to unexpected updates, but are not widely used in libraries and come with tradeoffs around duplication and maintenance.
  • Deterministic install workflows (npm ci, pnpm install --frozen-lockfile)
    Help ensure consistency, but only when a lockfile already exists and is respected.
  • Avoiding ad-hoc execution (npx, global installs)
    Reduces risk, but conflicts with common developer workflows and tooling expectations.
  • Short-lived exposure windows
    Make reactive defenses difficult. By the time an issue is identified, the vulnerable version may already be gone.
  • Minimum publish age / cooldown windows (where supported)
    Some package managers can delay installs of newly published versions, reducing exposure to short-lived malicious releases. For example, pnpm supports a minimumReleaseAge setting. Support is not standardized across ecosystems, and behavior can often be overridden via configuration (e.g. .npmrc), which limits its reliability.

The important point is not that these controls are ineffective. It’s that they are context-dependent.

They work well in controlled environments, but break down in exactly the kinds of workflows that are now common across modern development and CI systems.

These tradeoffs are well understood by maintainers. They are also exactly what attackers are beginning to exploit.

How to De-Risk Run Time Dependency Resolution#

For CI workflows

Preparation

  1. Create a root package.json (or use a dedicated npm workspace)
  2. sfw npm install <package>@version the desired packages
    1. a package-lock.json will be generated
  3. edit your package.json and pin every version to the specific one you are using
    1. Be aware that npm install defaults to ^ ranges.
  4. If you're not already using Socket's Firewall, this is a good time to run socket scan to verify safety.
  5. Commit and push your changes.

Execution

  • In every CI pipeline, make sure you run npm ci where the package-lock.json was committed.
    • DO NOT run npm install - this will override your package-lock.json and pull in new package versions (within applicable ranges).
  • Change all npx invocations in your CI pipeline to: npx --no --offline
    • --no will ensure not to install a package if it's not present in the local project dependencies
    • --offline Forces full offline mode. Any packages not locally cached will result in an error.
  • Depending on your setup, you might want to specifically point to a workspace where the CI relevant packages are installed:
    • npx --no --offline --include-workspace-root --workspace /path/to/ci-workspace

For local MCP packages

Preparation

  1. Create a dedicated directory to install and manage your dependencies, (e.g cd $HOME/mcp) create a new package.json (npm init --yes)
  2. Follow the same preparation steps from above

Usage

In your your AI Client configuration, for each mcp server that uses npx, ensure the following:

  1. Add the following arguments: --include-workspace-root --workspace $HOME/mcp --no --offline, to every npx invocation.
  2. For extra safety, never use latest! always specify the in the version of the package you want the AI agent to execute with npx.

Full example:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "--include-workspace-root",
        "--workspace $HOME/mcp",
        "--no",
        "--offline",
        "@playwright/mcp@v0.0.70"
      ]
    }
  }
}


Note:
You can also set npm_config_yes=false in a .npmrc or set NPM_CONFIG_YES=false as an env var to instead of using --no every time.

Where Install-Time Controls Fit#

One of the few places this type of attack can be reliably stopped is at install time.

In this incident, the risk existed only while the malicious version was live on the registry and being resolved by package managers. Once installed, the payload executed immediately. After removal, the version was no longer available to analyze or reproduce.

That makes traditional approaches less effective:

  • Static dependency analysis may miss short-lived exposure windows
  • Lockfiles only protect previously resolved dependencies
  • Post-install detection happens after execution

Controls that operate at install time address a different part of the problem.

For example, Socket Firewall intercepts package requests as they are made to the registry and checks them against known malicious packages and policy rules, blocking those that have already been identified as unsafe before they are downloaded or executed. (This tool is free, by the way, and we also offer enterprise support for additional features.)

This does not eliminate the underlying issues with dependency resolution, but it changes the outcome in scenarios like this one:

  • If a malicious version is introduced into a valid semver range
  • And resolved during a fresh install or npx execution
  • It can be blocked before the install completes

This is one of the few control points where short-lived supply chain attacks can be stopped before execution. This helps in scenarios like this, but it doesn’t change the underlying complexity.

A package as widely used as Axios being compromised shows how difficult it is to reason about exposure in a modern JavaScript environment.

  • You may have been affected without knowing it.
  • You may not be able to prove whether you were affected.
  • The window of risk may have been measured in minutes.

This is not a failure of one project or one team. It is a property of how dependency resolution in the ecosystem works today. And it is a problem that does not yet have a simple answer.

Sidebar CTA Background

Secure your dependencies with us

Socket proactively blocks malicious open source packages in your code.
Install

Subscribe to our newsletter

Get notified when we publish new security blog posts!

Related posts

Back to all posts