Supply Chain
Malicious Axios npm Releases and the Delivery Chain Git Never Saw
For three hours on March 31, 2026, npm delivered two unauthorized Axios releases whose source history did not exist in Git; one injected dependency then moved execution from an ordinary install into developer and CI identities, forcing responders to reconstruct the incident from locks, caches, processes, credentials, and every artifact the affected job produced.

In this article
1 The release that never existed in Git
1.1 A normal build with an abnormal provenance
At 00:21 UTC on March 31, 2026, a build could do everything its engineers had taught it to do and still become the first scene of an incident. It could start from a reviewed application commit, check out from the right organization, run the familiar package-manager command, complete without a test failure, and produce an artifact that looked ordinary. The dangerous object did not enter through a pull request. It arrived through the package registry under the name of a library the team already trusted.
The object was [email protected]. A second unauthorized release, [email protected], followed on the older branch around 01:00 UTC. Neither version had the source tag, Git commit, release discussion, or normal workflow record a reviewer would expect. Microsoft Threat Intelligence and the Axios maintainer postmortem describe a compromised maintainer account publishing both directly to npm. Their manifests added [email protected] as a runtime dependency. Axios did not import that package. It did not have to: the dependency's installation lifecycle supplied the execution edge.
That is the contradiction that makes this incident worth studying beyond its three-hour registry window. The Axios source a developer read and the Axios package a build received were two different custody objects. Git could honestly show no malicious commit while npm honestly recorded that it had served a package carrying a malicious dependency. A source review that stopped after searching the Axios repository for the payload would therefore produce a reassuring answer to the wrong question.
The right first question is narrower: which exact bytes did this install resolve? Answering it requires the lockfile or resolution record, registry URL, integrity or shasum, cache key, install time, and package-manager behavior. Only after fixing that object can the investigation ask whether a lifecycle script ran, whether its network transfer succeeded, which identity the process inherited, and what left the machine afterward. Each transition has separate evidence, and collapsing them turns both false alarms and missed compromises into policy.
Microsoft attributes the infrastructure and account behind plain-crypto-js to Sapphire Sleet. The Axios maintainer describes a targeted social-engineering campaign and compromise of a lead maintainer device. Those are source-attributed assessments about actor and access. They do not change the mechanical proof required for an individual organization: a lock or cache can establish package selection; endpoint or runner telemetry can establish execution; network and file records can establish a second stage; identity and artifact histories can establish the consequences. This report keeps those questions independent.
The public timeline is compact. A clean-looking [email protected] appeared at 05:57 UTC on March 30. Version 4.2.1, with its install hook, followed before the unauthorized Axios packages. Community reports began around 01:00 UTC on March 31. The maintainer postmortem records removal of the Axios versions around 03:15 UTC and removal of the dependency at 03:29. A small publication window, however, does not mean a small investigative window. A generated lock, a warmed cache, a persistent workspace, or an image layer can keep the selected object in circulation after the registry stops resolving it.
The opening scene therefore has two times. The registry incident lasted hours; a particular organization's exposure lasts from its first affected resolution until the last credential, cache, artifact, deployment, and derivative output has been dispositioned. That second clock is the one security and engineering leaders must sign off. It begins with the installation record and ends with a rebuilt, independently attested lineage—not with an npm takedown notice.
1.2 The investigation follows objects, not the familiar package name
Package names make convenient search terms, but they are poor evidence identifiers. The string “axios” may refer to a semver range in a manifest, an exact resolution in one of several lock formats, a cached tarball, an unpacked directory whose manifest was later changed, a bundled copy in an output, or a registry release carrying provenance. Two of those objects can share a version label while differing in bytes; two byte-identical files can arrive through different custody paths. A serious review gives each object its own row.
For the source row, SOSEC fixed the clean 1.x release at tag v1.14.0, commit 46bee3dea75ef53a8eae49f3b7487e6341de6074. Its manifest declares the expected runtime dependencies—follow-redirects, form-data, and proxy-from-env—and no plain-crypto-js. For the clean registry row, the 1.14.0 tarball obtained during review matched the registry SHA-1 and produced SHA-256 90e97a3194f24251d54a0b751fb2f1e38ce1427dfa15a6098f2f1b064274de2a. That baseline is not an attempt to reconstruct deleted malware; it establishes what the last authorized release looked like.
The malicious rows require a different evidence label. npm no longer served 1.14.1, 0.30.4, or 4.2.1 during SOSEC review, so this report does not claim a local execution of those removed tarballs. Their manifest-only Axios change, retained shasums, postinstall script, string-decoding functions, platform branches, cleanup behavior, and second-stage observations come from Microsoft and researchers who retained the packages while they were available. The maintainer's timeline independently fixes publication and removal. Stating that handoff is stronger than silently treating a screenshot or third-party archive as a local reproduction.
Once the objects are separated, the incident becomes easier to reason about. The clean source tells us what maintainers intended. Registry metadata tells us what could be resolved. A lock tells us what one dependency graph selected. The package manager tells us which lifecycle policy applied. Process and network records tell us whether execution progressed. A cache or image layer tells us whether the bytes persisted. A credential provider and deployment system tell us which authority became available and which outputs left the runner for downstream systems. No single scanner has to infer all of it.
Tracking each custody object separately also prevents an overcorrection. A project whose manifest permitted 1.14.1 was eligible to select it, but eligibility is not proof that it did. A lock fixed at 1.14.0, an offline mirror that never synchronized the release, or an install executed with a frozen lock can keep the malicious object out. Conversely, a repository that currently shows 1.14.0 may still have built 1.14.1 during the window and later reverted. Current manifests alone cannot close either case.
The story now has a precise path forward. First establish when the registry object became available and how a resolver could retain it. Then walk the dependency into npm's lifecycle, where an unused runtime dependency still executes. From there, map the installer to the runner's credentials and every artifact promoted from that job. The source/registry contradiction is not a footnote; it is the thread that connects all eight chapters.
2 Four clocks replace the comforting three-hour story
2.1 Publication is an opportunity, not proof of installation
The phrase “live for about three hours” is accurate for the two Axios registry entries and misleading for almost everything an operator needs to decide. Registry publication defines when a fresh resolver could first select an unauthorized version. It does not say whether a particular repository admitted that version, whether a lock pinned it, whether a cache already held it, whether scripts were enabled, or whether a build ran while the object remained reachable. Those facts live on other clocks.
The first clock belongs to the registry. The maintainer records 1.14.1 at 00:21 UTC, the 0.30.4 publication around 01:00, Axios removals around 03:15, and dependency removal at 03:29. Registry metadata and retained package analysis bound the opportunity for an ordinary online resolution. This clock matters when querying package proxies and resolver logs, but it cannot by itself turn every project with a compatible range into an affected host.
The second clock belongs to dependency resolution. A manifest range such as ^1.14.0 can admit a later 1.x release; ^0.30.0 can admit later 0.30.x releases. A package manager operating without an applicable lock may choose the newest matching version available from its configured registry or mirror. A frozen install with a committed lock is different: it should request the object already named by the lock and verify its integrity. The exact command, package-manager version, workspace topology, lockfile format, and registry configuration all belong in the evidence row.
The third clock starts when installation reaches a machine. Downloading or unpacking a dependency is not identical to running its lifecycle script. The project's configuration may set ignore-scripts=true; an invocation may include the corresponding command-line policy; a newer allow-scripts mechanism may require explicit approval; or a sandbox can deny process and network effects. In the default behavior described by npm for the affected period, however, eligible dependency scripts execute during an ordinary install. Process telemetry is the best separator between “selected” and “ran.”
The fourth clock belongs to consequences. A second stage can persist beyond the package process. A stolen secret can be used hours or weeks later. A container or release artifact produced by the job can be promoted after the runner disappears. A poisoned cache can feed another job after npm removes the source release. An investigation window therefore extends through the useful lifetime of identities and outputs touched by the install, not merely through the registry availability interval.
| Clock | Question it answers | Evidence that closes it |
|---|---|---|
| Registry | When could a configured resolver newly obtain the unauthorized release? | Publication/removal records, mirror synchronization, proxy and registry logs. |
| Resolution | Which exact package object did this dependency graph select? | Lock commit, resolved URL, integrity/shasum, cache key, package-manager output. |
| Execution | Did the lifecycle hook run, and did the next transfer succeed? | Process tree, script policy, file writes, DNS/proxy/EDR telemetry, runner logs. |
| Consequence | Which credentials, shared state, outputs, and deployments remained at risk? | Secret injection records, identity use, cache lineage, artifact digests, promotion history. |
The four clocks also explain why incident counts often disagree. A registry may report downloads. A source-code scanner may report manifests whose ranges admitted the release. An SBOM may report a resolved component. An EDR may report the install script. A cloud audit trail may report use of a credential that was available to the affected job. Each count has a valid denominator; none should be relabeled “victims” without the joins that connect it to the next clock.
2.2 A lock and cache can outlive a removed release
A lock generated inside the publication window can preserve the unauthorized resolution after npm stops advertising it. Whether later installs can retrieve the tarball depends on the configured source and cached copies. An internal proxy may retain a blob; a developer cache may satisfy an install; a CI cache may restore an unpacked tree; a container layer may already contain the package. Takedown changes future registry resolution. It does not revoke every stored copy.
A useful lockfile search must retain more than the version line. Capture the repository and branch, lockfile path and format, commit that introduced the resolution, resolved URL, integrity or shasum, workspace package, and dependency path that required Axios. Preserve enough surrounding structure to distinguish a real package entry from documentation, test data, or a scanner signature. Where the lock was later rewritten, Git history and build-artifact metadata can recover its state at build time.
Caches need the same care. Deleting a local node_modules directory can erase useful timestamps and manifest residue before the team knows whether the hook ran. A package-manager cache may contain index metadata, content-addressed blobs, and access times. CI platforms often key caches from lock hashes, branch names, tool versions, or user-provided strings; those keys can show which subsequent jobs restored the affected state. Preserve the relevant entries and platform audit record before invalidating them.
The malicious dependency's cleanup behavior makes this preservation especially important. Retained-artifact analyses report that setup.js attempted to remove itself, remove the manifest carrying the hook, and replace it with a clean-looking package.md that claimed version 4.2.0. An investigator arriving after execution could therefore find a directory whose self-reported version no longer matched the object originally installed. The parent lock, cache metadata, file-system timeline, process tree, and network record become more trustworthy than the rewritten child manifest.
Repository history now serves as a timeline, not an acquittal. Search every active and release branch, tags used by build systems, archived repositories, generated lockfiles, deployment bundles, SBOMs, and dependency-update pull requests. A clean current default branch closes only the present state. The evidence question is whether any buildable state resolved the unauthorized object and then reached an installer under a useful identity.
Mirror operators should export synchronization and request records before normal retention removes them. Record upstream URL, package/version, timestamp, requesting principal, response status, content digest, and eviction state. If the mirror never obtained the malicious object, it may provide strong exclusion evidence for clients configured exclusively to use it. If it did, the mirror's own retention can extend the window beyond the public registry removal and must be included in cleanup.
At the end of this chapter, the short incident has become a set of answerable cases. A compatible range with no resolution record is a lead. A matching lock is confirmed package selection. A cache restore or package fetch puts bytes on a machine. A lifecycle process proves execution. A platform artifact or connection proves the next stage. Credential and promotion records define the organizational consequence. That ladder carries us to the next question: how to establish the delivered object's custody when the malicious tarballs themselves have been removed.
3 The delivered object becomes the center of the evidence room
3.1 Fix the clean baseline before describing the divergence
When the malicious registry entries are gone, the temptation is to begin with screenshots of their manifests and work backward. That reverses the burden of proof. A better investigation first fixes an authorized object whose custody can be reproduced end to end. For Axios 1.x, tag v1.14.0 points to commit 46bee3dea75ef53a8eae49f3b7487e6341de6074. The repository contains the source, build scripts, release workflow, and manifest from which an authorized release was expected to emerge.
The manifest at that revision provides a simple negative fact with large consequences: the runtime dependency block contains follow-redirects, form-data, and proxy-from-env, not plain-crypto-js. A repository-wide search also has no legitimate Axios import of the injected name. This does not prove what the deleted 1.14.1 tarball contained. It proves that the dependency documented by the incident sources was not part of the last clean source line and had no application-runtime role to explain its appearance.
The clean registry package adds a second custody point. During review, npm metadata for 1.14.0 identified the same Git head and supplied shasum, integrity, and provenance fields. The retrieved tarball's SHA-1 matched the registry shasum; its SHA-256 was 90e97a3194f24251d54a0b751fb2f1e38ce1427dfa15a6098f2f1b064274de2a. Its manifest and built files were consistent with the clean tag. These observations let an operator identify a known-clean package object without relying on “latest” or a mutable web page.
The pre-incident workflow explains the expected publishing path. A version tag triggered checkout, dependency installation, build, and npm publish --provenance. The publish job also received a long-lived npm token. In an authorized release, a reviewer could look for the tag, the workflow run, the commit, and the registry object. The two incident versions lacked that ordinary source trail and trusted-publisher binding, while the compromised account still possessed enough registry authority to publish them directly.
A missing trail is an anomaly, not a magic detector. Older releases or emergency workflows can have incomplete provenance for legitimate reasons. The value comes from comparison: a package whose project has an established tag-and-workflow convention appears without either, adds a previously unseen runtime dependency, and is published from a path the project's own postmortem says was unauthorized. Independent anomalies converge on the same delivered object.
The 0.x branch deserves its own clean reference. Organizations that legitimately stayed on 0.30 should not assume the 1.x tag describes their graph. The maintainer recommends 0.30.3 as the safe predecessor to 0.30.4. A response inventory should preserve the clean object, lock structure, and release stream for each application; forcing all evidence into a 1.14.0 frame would erase that distinction. The incident mechanism is shared, while the dependency graph and upgrade decision remain application-specific.
A fixed clean baseline also makes later rebuilding measurable. A team can clear the affected cache, restore a reviewed lock that resolves a clean Axios object, install under a policy that blocks unapproved scripts, and compare the new output digest with the artifact previously produced. If the output changes, the difference requires explanation. If it does not, the rebuild still matters because it replaces an untrusted execution lineage with one whose inputs and identity can be attested.
3.2 Removed malicious packages narrow reproduction, not the public conclusion
At the time of SOSEC review, querying npm for 1.14.1, 0.30.4, and 4.2.1 returned no live version object. That absence creates a publication discipline. We did not fetch a copy from an unofficial archive, did not treat an unknown mirror as authentic, and did not execute a package merely because its filename matched. The report instead marks which conclusions come from local official objects and which come from incident researchers who retained the packages during availability.
Microsoft reports a surgical manifest change: the Axios source and built bundles remained unchanged while the runtime dependency pointed at [email protected]. The Axios maintainer independently confirms the two versions, injected dependency, publication account, and removal timeline. StepSecurity's retained comparison supplies package shasums and a line-level analysis of setup.js, including the postinstall entry, decoder functions, platform selection, process launch, and attempted manifest replacement. Agreement across these records supports the delivery-chain conclusion without pretending the original bytes remain obtainable from npm.
There are still claims the available evidence does not support. A download count cannot identify every organization that executed the script. A package directory that says 4.2.0 cannot exclude earlier 4.2.1 execution because the cleanup routine reportedly wrote that value. The existence of a matching lock does not prove the second stage connected. Actor attribution does not prove a particular endpoint belonged to the same operator at every later date. Each limit belongs beside the fact it qualifies; a generic disclaimer would hide which limit applies to which claim.
Organizations may possess stronger private evidence than the public record. An internal artifact repository can retain the exact tarball and integrity metadata. EDR can retain the script's process ancestry and file writes. A proxy can retain the destination and response. Those artifacts should be hashed, access-controlled, and linked to the case without being redistributed as samples. Their custody can upgrade an individual case from a source-attributed mechanism to locally reproduced execution, while the public article remains within what its cited sources establish.
Time is normalized in the same ledger. Registry timestamps use UTC; workstation clocks may use a local zone; pipeline interfaces may render the viewer’s locale; endpoint events can arrive late after a runner reconnects. Each retained record keeps the original timestamp, stated zone, normalized UTC value, and collection source. A three-hour publication window is too narrow to tolerate a silent eight-hour conversion error, especially when teams are deciding whether a build ran before or after removal.
Integrity fields also need names. npm’s historical shasum is SHA-1 over the package archive, while the integrity field normally carries a stronger subresource-integrity value. A local SHA-256 computed during evidence collection is a separate investigator digest. Recording only a long hexadecimal string invites the wrong comparison. The ledger stores algorithm, encoded form, exact hashed object, and source so a clean tarball digest is never compared with a manifest blob or an unpacked directory hash.
The registry URL is part of the object identity. A public npm URL, an enterprise mirror, a virtual repository, and a local cache can all satisfy the same package name and version while serving through different custody paths. Lock formats vary in how they record the source. Where a lock omits it, mirror access logs and package-manager configuration fill the gap. This matters both for exposure and recovery: a clean version fetched through an untrusted substitute is not automatically the same evidence object as the verified upstream archive.
Package contents should be compared as both bytes and structure. Archive digest equality is the strongest simple comparison for a fixed object. When repacking or metadata normalization prevents byte equality, investigators compare normalized file lists, modes, paths, manifest fields, built bundles, source maps, and provenance subject digests, documenting every transformation. A superficial source diff can miss a manifest-only change; a manifest-only review can miss an altered bundled file. The comparison method follows the way the package actually executes.
Collection itself must avoid creating a new event. Registry metadata and known-clean official objects can be retrieved in an isolated review process. Removed malicious artifacts should not be hunted through random archives, installed on a developer machine, or queried against historical infrastructure merely to make the report feel complete. If an organization already retains a suspect cache entry, analysts work from a copy under controlled malware procedures and preserve the original. Public reconstruction stops where safe, authenticated custody stops.
4 One unused dependency opens a six-step execution chain
4.1 Semver and the lock decide which parcel arrives
The chain begins before any malicious JavaScript runs. A root application, workspace package, or transitive dependency declares an Axios range. The package manager combines that range with its lock policy, configured registry, workspace state, and available versions. During the publication window, a fresh resolution that admitted 1.14.1 or 0.30.4 could select the unauthorized release. A frozen lock pointing at 1.14.0 or 0.30.3 would request the clean object instead.
The distinction between npm install and npm ci matters, but the command name is not enough. npm documents ci as requiring an existing lock and refusing to rewrite it when package metadata disagrees; teams often use it for reproducible automation. Workspaces, omitted dependency classes, legacy peer-dependency flags, alternate registries, and package-manager versions still affect the resulting tree. Record the actual command line, tool version, environment configuration, and lock commit from the job; a pipeline template cannot supply those facts.
After the unauthorized Axios tarball arrives, npm reads its manifest and adds the declared runtime dependency to the graph. No source file in Axios must call require("plain-crypto-js"). Package installation operates on manifest edges, not application import reachability. A “zero imports” finding is therefore suspicious but not protective: the dependency's purpose is to be installed, and its own manifest defines behavior the package manager will invoke.
The clean-looking 4.2.0 release adds social and automated cover. A registry account with one previous version does not look as new as an account whose first publication is immediately referenced by a major package. Version 4.2.1 then introduces the install hook. Detection systems that score only the age of a package name or the popularity of its direct parent can miss the decisive difference between adjacent package versions.
Lock review must therefore traverse to the exact child. A lock can name Axios 1.14.1 and plain-crypto-js separately, with a resolved URL and integrity value for each. Some formats place package entries by path; others maintain a resolution table. An SBOM generated before installation may capture only the intended graph, while an SBOM generated from the installed tree may see the rewritten 4.2.0 manifest. The parent lock and cache digest are the better historical anchors.
A repository may never name Axios directly. Frameworks, SDKs, CLIs, test harnesses, and internal packages can bring it transitively. An organization-wide query should therefore combine direct manifest searches with lockfile parsers, dependency graph exports, artifact/SBOM inventories, and registry proxy records. The owner of the affected build is the owner of the runnable graph, not necessarily the team that chose Axios.
4.2 The lifecycle hook inherits the installer's authority
The critical transition occurs when npm processes postinstall: node setup.js. For the relevant default behavior, eligible scripts run unless policy disables or restricts them. The script is not confined to the permissions an application later receives in production. It runs as the user or service identity performing dependency installation, inside the developer workstation or build environment, with that process's environment, filesystem view, network route, and injected credentials.
Retained-artifact analysis describes two string transforms used to hide module names, platform identifiers, paths, and commands. The entry routine reconstructs the strings at runtime, loads child-process, operating-system, and filesystem functions, identifies the platform, chooses a delivery path, launches it, and handles failures broadly. Obfuscation changes what static review sees. It does not change the process ancestry an endpoint or runner can record: package manager, lifecycle shell where applicable, Node, then platform-specific children.
The macOS branch reportedly wrote a native payload at /Library/Caches/com.apple.act.mond, a name designed to blend with system-looking files. The Windows branch staged material in the temporary directory, created %PROGRAMDATA%\wt.exe, and led to a reported user Run-key persistence path. The fallback branch wrote /tmp/ld.py and launched it detached where shell, transfer utility, and Python dependencies were present. Microsoft publishes platform hashes that defenders can use with appropriate historical context.
All branches contacted the same published endpoint and used distinct POST bodies to select a response. The destination is useful for historical DNS, proxy, firewall, and EDR queries. It is not safe to assume an address or domain remains actor-controlled indefinitely, and the article never instructs readers to request it. Detection should combine the destination with process ancestry, package timing, and created paths so that later infrastructure reassignment does not turn an old indicator into an unqualified block.
A blocked outbound connection changes the consequence grade. The unauthorized Axios object still resolved, the dependency still installed, and the hook may still have executed. If network control prevented the response and no alternate stage arrived, the evidence can support containment before second-stage success. Credentials present in the process remain worth reviewing for local access or logging exposure, but automatic rotation decisions can be prioritized differently from a case that shows a payload, child process, persistence, or later identity use.
The cleanup routine is the last technical step and the first forensic trap. It attempts to remove setup.js, remove the manifest containing the lifecycle script, and rename a clean-looking file over package.json. The replacement reports 4.2.0 and omits the hook. An after-the-fact inventory that trusts only installed manifests can therefore downgrade the very evidence it needs. Parent locks, package-manager caches, file journal data, endpoint telemetry, and build logs become primary.
Script policy is a strong break in this chain but needs operational precision. A global ignore-scripts setting can prevent required native builds or setup tasks and may be overridden in some workflows. Newer npm controls can approve particular packages. The durable policy is to enumerate which dependencies genuinely require installation code, approve them by reviewed version or controlled package identity, and fail a build when an unreviewed transitive package introduces a lifecycle script. The Axios repository's post-incident threat model explicitly notes that deliberately invoked build plugins remain executable even when lifecycle scripts are disabled.
Network policy supplies another independent break. A build runner that can fetch only from an internal package mirror and approved build services cannot freely reach an arbitrary second-stage host. Such a policy must account for legitimate compiler downloads, browser binaries, container registries, and source hosts, then measure exceptions. A deny event should retain process identity, destination, job, repository, and lock digest so it becomes actionable evidence; an anonymous firewall count cannot support the same conclusion.
At this point the chain has crossed from package custody into a machine identity. On a developer endpoint, that identity may have source, SSH, cloud, and password-manager access. In CI it can be short-lived yet highly privileged, carrying federation tokens, registry rights, signing keys, or deployment connections. The next chapter follows those privileges and shows why deleting an ephemeral runner is not the end of the incident.
5 A disposable runner can leave a durable incident
At installation, the dependency stops being an unexpected line in a registry manifest and becomes code running as the identity that assembles software. On a laptop that identity may be a developer. In continuous integration it may exist for only minutes, yet during those minutes it can read a source checkout, receive short-lived cloud claims, pull private packages, sign an artifact, push an image, or invoke a deployment connection. The runner may disappear while every decision it made continues downstream.
Destroying an ephemeral runner is necessary but incomplete. Destruction removes a possible persistence surface. It does not retract a registry token already presented to a process, invalidate an image already pushed, erase an artifact already promoted, or prove that a federation token was never copied. The incident has to be reconstructed as a trust graph whose first node is the affected install and whose outward edges are identities, stores, outputs, and later automation.
The graph is different for every organization, but its vocabulary is stable. A job has a source revision, a lock digest, a runner identity, a set of injected variables and mounted files, a network policy, cache inputs and outputs, artifact destinations, signing operations, deployment permissions, and human owners. Capturing those objects converts a frightening phrase such as “CI was exposed” into a finite set of questions that can be answered and closed.
5.1 Build authority is larger than the application permission set
Application teams often reason from production runtime permissions. A browser bundle cannot read the build server's environment after it has shipped; a container may start as an unprivileged user; a serverless function may receive a narrow role. None of those later restrictions describe the process that installed dependencies. The install process runs earlier, when the system is intentionally trusted to transform source into an artifact and is therefore surrounded by powerful material.
The runner may receive an npm credential used to reach a private scope, a Git token capable of fetching submodules, an SSH agent, an OIDC assertion exchangeable for cloud credentials, a container-registry password, a code-signing interface, a release key, a deployment webhook, or a package mirror client certificate. Sensitive information can sit beside those credentials: unreleased source, customer-specific configuration, internal hostnames, test data, and the names of future products. A review that searches only for obvious static secrets misses the short-lived and ambient authorities modern pipelines prefer.
Short-lived credentials improve the system by narrowing reuse time, but they do not make the affected job harmless. A token valid for ten minutes can still perform a privileged operation during those ten minutes. Responders should preserve the identity provider's issuance and exchange records, record the exact audience and claims, identify which role was obtained, and review operations performed from the install time until the credential expired. Rotation is not always the right verb for a federated identity; removing trust, narrowing claims, invalidating sessions, and examining role activity may be the equivalent response.
Service connections deserve the same precision. Some build systems inject a secret into the process. Others broker a task-scoped connection without exposing a reusable value. Some restrict the connection to an environment with an approval gate, while older templates make it available to every step in a job. The consequence grade should follow the actual delivery mechanism. “The project has production access” is not proof that the malicious process received it; “the deployment step ran in the same job with an automatically authorized connection” is a much stronger join.
Self-hosted runners add another layer. A persistent workspace may retain the rewritten dependency tree. A shared npm cache may carry the removed tarball or metadata into a later job. A long-running agent can preserve process, file, and network telemetry that a hosted runner loses on deletion. It may also have standing network routes and local credentials that were never intended to enter a disposable workload. The same persistence that increases risk can give investigators the evidence needed to distinguish resolution, execution, delivery, and later use.
Hosted runners move the evidence to other systems without eliminating it. Microsoft’s Azure Pipelines guidance distinguishes the hosted service from the compromised package and directs customers to the jobs that installed it. Pipeline logs, job metadata, cache records, identity-provider events, artifact registries, source audit logs, and downstream deployment systems can still reconstruct the graph. The question is not whether the virtual machine remains; it is whether the organization retained the records around it.
5.2 Outputs inherit doubt until lineage breaks it
A successful build is not evidence of a clean build. The Axios application code itself could compile and test normally because the injected dependency did not need to alter Axios functions. A malicious lifecycle process could run beside an otherwise valid transformation. Test success proves that expected behavior survived; it does not prove that no additional behavior occurred during installation.
Every output produced after an affected install enters review: JavaScript bundles, container images, native packages, SBOMs, checksums, signatures, test reports, caches, and intermediate archives. This does not mean each output is proven modified. It means their clean lineage has been interrupted. Rebuilding from a verified source revision and lock, on a clean runner with restricted egress and newly issued authority, creates a comparison object that can restore confidence.
Digest equality is helpful but must be interpreted. If a deterministic output rebuilt in a clean environment matches the affected output byte for byte, confidence rises that the published payload did not alter that output. The match does not erase credential exposure or other side effects. If outputs are nondeterministic, a digest mismatch is expected and cannot by itself prove tampering. Teams then compare structured content, build manifests, provenance, file lists, compiler inputs, and signing events.
Signatures also need lineage. A valid signature answers which key or service approved an object; it does not establish that the process requesting the signature was clean. If an affected job could invoke signing, investigators review every signing request after the install time and, where possible, bind signing authorization to a reviewed workflow identity, source commit, and attested build. The repair is not merely to re-sign the old artifact. It is to rebuild and sign a new object whose path can be explained end to end.
Cache handling creates one of the easiest reinfection loops. Removing the version from npm stops new registry resolution, but a content-addressed mirror, package-manager cache, persistent workspace, or container layer may still satisfy a later job. Deleting by package name is insufficient when entries are keyed by URL, integrity value, lock digest, or opaque cache key. Responders should enumerate all cache namespaces reachable from affected jobs, preserve a forensic copy where policy permits, invalidate the operational copies, and force the clean rebuild to demonstrate a fresh resolution path.
The same reasoning applies to locks created during the window. A removed package can remain selected because the lock remembers its exact object. Repositories, generated examples, archived release branches, and template projects may preserve that selection even if the default branch has moved on. The investigation should treat a poisoned lock as an active delivery instruction until it is replaced, reviewed, and propagated to every branch or consumer that can still build.
At the end of this chapter, the incident is no longer bounded by three hours of registry availability. Its operational duration is the time until the last affected lock, cache, credential, and derived output is either disproved, invalidated, or rebuilt. That longer clock is why the publishing repair matters: it must make the next divergence both harder to create and easier to observe.
6 The repair added witnesses to every release
The clean Axios revision immediately before the incident preserves the old release arrangement. A version tag triggered a GitHub Actions workflow that checked out the repository, ran npm ci, built the project, and published with provenance. The publish step also received a long-lived NPM_TOKEN. That workflow could produce a well-attributed normal release, but possession of a publish-capable account credential still permitted a different route: a registry publication that had no matching tag, workflow run, or source commit.
The malicious versions used that silent route. Their most revealing property was not an exotic obfuscation technique; it was the absence of the ordinary release story. There was no corresponding Axios development change, no source tag explaining the version, and no expected workflow identity binding the package to a repository event. A consumer that checked only the package name and semver saw a plausible update. A consumer that demanded agreement among source, workflow, provenance, and registry would have seen a missing witness.
After the incident, Axios changed more than one control because no single control covers the whole path. Commit ebf30369328b61c7ea9d152d4d5afa7d26e59c06 removed the long-lived publish token from the workflow and moved publication to npm trusted publishing through OIDC. Later history pins workflow actions, narrows permissions, provides the legacy release line with its own trusted path, and places publication behind an npm-publish environment. Commit 302e2739c602f00e323d4f3f5c79500647633a73 adds the repository-level ignore-scripts=true policy.
6.1 OIDC changes who may speak for the repository
A static registry token is a bearer secret. Whoever obtains it can attempt to publish until it expires or is revoked, subject to registry policy. A trusted-publishing exchange instead asks npm to accept a short-lived identity issued for a named repository and workflow under defined conditions. The useful security property is not simply that the credential expires quickly. It is that the registry can require the request to arrive through a particular automated path.
The OIDC, tag, and protected-environment path becomes an enforceable release object. Reviewers can ask whether a published version has a matching tag, whether the trusted workflow ran, which commit it checked out, which environment approved it, what permissions it held, and whether provenance binds the package to that execution. If the registry contains a version but the source and workflow objects do not, the discrepancy becomes a first-class incident signal; it no longer waits to be explained away after compromise.
Provenance is one witness, not a verdict. It can bind the built object to a workflow and source revision. It does not prove that every dependency executed during that workflow was safe, that the source review was adequate, or that a compromised workflow could not produce a malicious artifact. Its value comes from making claims explicit and comparable. Consumers can enforce that the claim exists, matches the expected repository, and refers to an approved build path; maintainers can detect a release that lacks it.
Protected environments add a deliberate pause at the point where a build acquires publication authority. A human or policy engine can inspect the tag, changelog, tests, package contents, and provenance preview before the registry accepts the object. The pause is most useful when approval is independent of the account that prepared the release and when the environment grants authority only after approval. A decorative approval that occurs after credentials are already available does not close the same edge.
Action pinning and narrow workflow permissions reduce the number of moving parts allowed to alter the release. A branch name or mutable action tag can later point somewhere else; a fixed commit records the exact external workflow code selected. Narrow permissions prevent a build step from inheriting repository writes it does not need. Neither control protects against every compromised dependency, but both constrain the authority available if a build-time component behaves unexpectedly.
6.2 Script policy closes one door and names the doors left open
The repository-level ignore-scripts=true setting addresses the mechanism used in this incident. During dependency installation, npm lifecycle hooks no longer run by default for the project. The current release flow also installs with an explicit script restriction. Repetition is intentional: the repository policy protects ordinary contributors, while the command-line choice documents the release job’s expectation at the point of execution.
The setting carries operational costs. Packages with legitimate native builds, binary downloads, or setup tasks may fail or behave differently. A mature implementation does not respond by turning scripts back on globally. It inventories the packages that genuinely need installation code, constrains them to reviewed versions or controlled builders, and makes the exception visible. An unexpected transitive lifecycle hook should fail the build and create a review event, not quietly expand the executable set.
Axios’s current threat model is valuable because it states the residual gap. Disabling lifecycle scripts does not make the entire JavaScript build declarative. The project deliberately invokes Rollup, Babel, tests, linters, and their plugins. Code loaded by those tools runs with the build process’s authority because the workflow asks it to run. An attacker who compromises an explicitly invoked plugin does not need postinstall. Lock enforcement, dependency review, restricted egress, runner isolation, and reproducible outputs remain necessary.
Separating the exact exploit mechanism from the threat model prevents a common post-incident mistake. The March chain used a phantom runtime dependency and a lifecycle hook, so blocking hooks would have stopped it. The broader supply-chain problem is any unreviewed code that executes while a trusted machine constructs or publishes software. The repaired design should make all such execution enumerable, minimal, and surrounded by evidence.
Frozen installation is the next witness. npm ci treats the committed lock as a contract and fails when the manifest and lock disagree. It removes the existing dependency tree, then installs the exact recorded graph without opportunistic updates. This sharply reduces surprise resolution, but it protects only when the lock itself was created under review. A lock generated during the malicious window can faithfully reproduce the poisoned object; history, review ownership, resolved URLs, and integrity values still matter.
An internal registry mirror can add quarantine and retention. It may delay newly published versions, require provenance, scan lifecycle scripts, preserve removed metadata, and give incident responders a stable record of which projects fetched which object. It can also become a powerful trust concentration. The mirror must authenticate upstream objects, protect its own administrative path, keep immutable logs, and make policy bypasses observable. “It came from our mirror” should mean the object passed stated checks, not merely that the mirror cached it.
The strongest repair is therefore a set of disagreements that stop the line. A version without a protected tag is rejected. A publication without the trusted workflow is rejected. A package without expected provenance is rejected. A lock change without review is rejected. A new lifecycle script is rejected. An install process that reaches an unapproved destination is blocked and recorded. Each check asks a different system to corroborate the release, so compromising one record no longer produces a complete story.
Durable receipts also shorten future investigations. Instead of reconstructing everything from a vanished runner and a self-rewritten manifest, responders can compare a small set of fixed records. The technical lesson of Axios is not that Git was useless; it is that Git told the truth about one object while the registry delivered another. The repair makes those objects meet before a consumer trusts either one.
7 Response begins by joining records, not by counting downloads
Once the versions were removed, the easiest numbers became the least decisive. Registry download counts can estimate how often an object was requested. A compatible semver range can estimate how many projects were eligible to select it. Neither establishes that a particular machine received the object, allowed the hook to execute, obtained a second stage, exposed an identity, or produced a promoted artifact. The response has to move through those questions in order.
The unit of investigation is an install event. Each event binds a repository and revision to a lock, a package source, a time, a host or runner, an install command, a script policy, a process tree, and an output set. Without that join, organizations tend to produce two bad lists: every repository that ever mentioned Axios, and every machine that ever ran Node. Both are large; neither tells an owner what to do next.
Start with three package identities: [email protected], [email protected], and [email protected]. Search manifests, locks, package-manager metadata, internal mirrors, SBOM stores, archived branches, release tags, container build inputs, template repositories, and caches. Retain resolved URL and integrity or shasum where the format supplies them. The dependency name alone remains useful because the cleanup routine could make its local manifest claim the earlier clean-looking version.
7.1 Grade the chain one transition at a time
A practical case model assigns evidence grades instead of a single exposed/not-exposed flag. Grade zero is eligibility: the project had a compatible range but no evidence that it resolved during the window. Grade one is resolution: a lock, mirror, or cache record identifies an unauthorized Axios version or the injected dependency. Grade two is installation: package-manager or filesystem records place the dependency on a host or runner. Grade three is lifecycle execution: process telemetry, logs, or correlated file events show setup.js or its child chain. Grade four is second-stage activity: the published destination, staged path, payload hash, child process, or persistence event appears. Grade five is downstream consequence: credentials are used, source changes, a signature is issued, or an artifact is promoted after the affected build.
The grades are not a promise that lower cases can be ignored. A grade-one lock can reproduce the incident tomorrow if it still builds. A grade-two install may lack process telemetry because logging was absent, not because the hook did not run. The model communicates what is proved, what remains possible, and which evidence would move the case. It keeps a blocked network connection from being described as a successful payload while preventing missing telemetry from becoming an automatic clean bill.
| Evidence grade | What is established | Immediate action |
|---|---|---|
| 0 — eligible | A range or indirect dependency could have selected the release. | Locate locks and build executions; do not infer installation. |
| 1 — resolved | A durable record selected the unauthorized object. | Quarantine the lock/cache and identify every consuming build. |
| 2 — installed | The package graph reached a specific host or runner. | Preserve telemetry and enumerate authority available to that process. |
| 3 — executed | The lifecycle process or characteristic child chain ran. | Isolate, collect, revoke reachable authority, and inspect outputs. |
| 4 — delivered | A platform second stage or persistence indicator is present. | Conduct endpoint incident response and expand identity hunting. |
| 5 — consequence | Identity use or downstream artifact movement is observed. | Scope the affected control plane, rebuild, and recover from clean systems. |
Preservation precedes deletion. For a persistent runner or developer endpoint, isolate network access while collecting the project lock, cache index, package directory names and timestamps, process ancestry, DNS and proxy events, file journal or EDR activity, temporary paths, persistence locations, and relevant identity logs. A copied executable should be handled under malware procedures. The public indicators are sufficient for historical search; there is no defensive reason to contact the published endpoint or execute a removed package.
Time windows need two extensions. The first begins before Axios publication because the dependency’s clean-looking predecessor and malicious successor established the delivery path. The second continues after registry removal because locks, mirrors, caches, and container layers can keep supplying the bytes. Identity review begins at each observed install and runs through token expiry, revocation, or the last unexplained use. Artifact review continues until every output from the affected build is invalidated or replaced.
Credential response follows reachability, not fear. Export the exact environment variables, mounted files, service connections, OIDC exchanges, role assumptions, signing interfaces, package credentials, and deployment gates available to the job. Revoke reusable secrets and active sessions from a known-clean system. For federated paths, remove or narrow the trust condition and review issued sessions. For signing and publishing systems, inspect every request made after execution and compare it with an approved source and workflow record.
7.2 Close the incident with a clean competing history
Recovery creates a new lineage that can be compared with the affected one. Replace the poisoned lock with a reviewed resolution. Purge operational caches and persistent workspaces after evidence preservation. Start a clean runner from a trusted base, restrict its egress, inject only newly authorized credentials, install with frozen resolution and script policy, build the same source, and produce fresh provenance. The resulting artifact should have a documented owner and promotion decision.
Every affected output is then handled explicitly. If it was never published and can be deleted, record the deletion. If it was published but never deployed, revoke or mark it and remove promotion rights. If it was deployed, identify every environment and replace it with the clean rebuild. If a customer downloaded it, publish accurate scope and remediation based on the artifact’s actual reach. Silence in this ledger is not evidence that the output vanished with its runner.
Detection should preserve the incident’s structural signals. Alert when a package version appears without the project’s expected tag or provenance. Review new runtime dependencies that have no imports. Inspect lifecycle-script additions anywhere in the transitive graph. Correlate package-manager processes with unusual child interpreters and network destinations. Notice when an installed manifest changes during the same process tree. These behaviors remain useful after the published hashes and infrastructure age out.
Admission tests make the controls real. A test package with a tagless publication should fail provenance policy. A dependency that introduces an unapproved install script should fail frozen installation. A controlled build step that attempts an unapproved destination should be blocked and produce an attributable log. A release workflow without environment approval should lack registry authority. The test result belongs beside the written policy because it proves enforcement at the edge that matters.
Ownership completes the system. The package team owns source and release configuration; the platform team owns runner images, cache isolation, and identity injection; security operations owns telemetry joins and evidence retention; identity and cloud teams own revocation and audit; product owners decide how rebuilt artifacts reach customers. A single incident coordinator maintains the installation-event ledger so these views converge on the same facts.
The ledger should record negative evidence with the same care as positive evidence. “No connection observed” is useful only when the sensor, collection interval, host identity, and retention period are known. A runner whose DNS log expired cannot be placed in the same column as a runner whose complete network telemetry shows the request was denied. Likewise, an empty package directory after workspace cleanup says nothing about what ran earlier. Writing the sensor and coverage beside every conclusion prevents absence from being promoted into certainty.
Large monorepositories must be investigated as individual installation jobs because repository-level guesses blur distinct executions. One commit may launch several jobs with different workspaces, lockfiles, package managers, cache scopes, and credentials. Record each install command separately and associate only the outputs that descend from it. This prevents a documentation build with no publication authority from inheriting the consequence grade of a release build, while ensuring a small utility job with a powerful service connection does not disappear inside an overall “tests passed” status.
Package mirrors should answer historical questions directly: which upstream object was cached, when it entered, which integrity value it carried, which clients requested it, whether policy allowed it, and when it was quarantined. If the mirror rewrites metadata or repacks archives, retain both the upstream receipt and the served object. Otherwise a useful control becomes another source of divergence, and investigators cannot tell whether the bytes on a runner came from npm, a mirror transformation, or a stale local cache.
Evidence must be retained long enough to cover software promotion cycles. A package may be installed in a nightly build, signed days later, promoted after a release freeze, and deployed to an offline environment weeks afterward. Runner logs kept for only a day cannot support that lineage. Retention policy should be derived from the longest path between dependency resolution and final deployment, with immutable summaries kept for artifacts that remain supported beyond the raw telemetry window.
7.3 The first seventy-two hours have named decisions
- During the first four hours, stop fresh resolution without destroying evidence. Quarantine the three package identities in internal mirrors, freeze automatic dependency updates, preserve current registry and proxy logs, and identify builds running from locks created during the window. Teams should not mass-delete caches or reinstall projects at this stage. The immediate product is a timestamped inventory of possible install events and the owners who can explain them.
- In the same interval, publish one internal case definition. It should list the exact package versions, UTC window, cleanup caveat, required fields, evidence grades, and collection contacts. A short form asks for repository, lock commit, package manager, install command, job or host, cache key, script policy, credentials delivered, and output locations. Consistent input allows central responders to join cases instead of translating a different spreadsheet from every team.
- Between four and twelve hours, move confirmed resolutions to install-level triage. Preserve persistent hosts, request hosted-runner records, and query package-manager process ancestry. Export identity issuance and service-connection availability for matching jobs before their audit retention changes. Outputs from those jobs receive a temporary promotion hold. A hold is not a claim that the artifact is malicious; it prevents an uncertain lineage from becoming a larger customer problem while facts are collected.
- At this point, a decision owner assigns each event a grade and an evidence gap. “Resolved, execution unknown because process telemetry was absent” is an honest result. It triggers cache invalidation, credential reachability review, and a clean rebuild without being mislabeled confirmed payload delivery. “Lifecycle process observed, outbound denied” triggers stronger endpoint collection but can remain distinct from a host where a matching file and later identity use appear.
- From twelve to twenty-four hours, identity and artifact work proceeds in parallel. Identity teams revoke reusable credentials and inspect federated sessions reachable from grade-three and higher events. Release teams enumerate every digest, signature, registry push, and deployment descended from affected jobs. Endpoint teams collect platform indicators under established procedures. The central ledger records which action closed which edge, so rotating one token never gets mistaken for closing the entire case.
- A clean rebuild should begin as soon as inputs and authority are ready, not after every forensic question is answered. The replacement uses a reviewed lock, known-clean package objects, a fresh runner, restricted egress, and new credentials. It produces provenance and an output comparison. This creates a safe path for urgent business delivery while the investigation continues, and it prevents teams from requesting exceptions to promote an artifact whose history remains unresolved.
- By forty-eight hours, leadership should receive a denominator-aware report: eligible repositories, confirmed resolutions, confirmed installs, observed lifecycle executions, second-stage findings, reachable credentials, affected outputs, completed rebuilds, and cases lacking telemetry. Counts should include their collection coverage and last update time. A single “systems impacted” total discards the evidence ladder and almost guarantees correction later.
- By seventy-two hours, unresolved cases need explicit disposition before they age silently. The owner can acquire missing evidence, conservatively treat the event as executed, or document why available controls exclude a later stage. The choice, approver, residual risk, and next review date enter the ledger. Production holds are lifted only against a clean replacement or a documented exclusion, never merely because the public incident has left the news cycle.
After the urgent phase, the same ledger becomes the improvement backlog. Repeated missing process telemetry becomes a runner-observability task. Unknown credential delivery becomes an identity-injection inventory. Untraceable outputs become an artifact-lineage project. Cache entries without consumers become a mirror-audit requirement. Every investigation gap gains a system owner, acceptance test, and due date; a lesson-learned sentence cannot close it.
Collection also has to respect the evidence shape left by each package manager. npm’s package-lock, pnpm’s content-addressed store and lockfile, and different generations of Yarn locks and offline caches place fields in different locations and apply different cache semantics. A central rule can normalize package identity and time, but it should not reduce every format to a text search. Each tool needs a parser, sample, and validation case that emits common fields while preserving the original record, so a format error does not turn a genuine resolution into an ordinary documentation hit.
Branch scope cannot stop at the default branch. Maintained release lines, hotfix tags, customer-specific branches, detached commits retained by build systems, and archived repositories that can still publish may all carry locks created during the window. Repository owners should enumerate the set that can currently build; the central investigation then connects each member to actual jobs. This keeps historical noise from overwhelming response without losing an uncommon older line that still has release authority.
Host collection should preserve a replayable raw timeline, not only a table of indicator matches. Process creation, parent-child relationships, command lines, users, working directories, file creation and renaming, network requests, deny results, and telemetry coverage must remain sortable by time. Platform indicators help locate a case, but the order of these events distinguishes resolution, script execution, and payload arrival. A digest match without its source path cannot by itself show that the file came from this installation.
The credential owner has to answer a narrower question: at which step, in what form, and at what time did the job give a particular permission to a particular process? Asking only whether the job “had secrets” obscures the difference among job-level environment variables, step-scoped secrets, mounted certificates, brokered service connections, and runtime OIDC exchanges. Recording the delivery model lets teams revoke the high-value authority that was actually reachable, while treating cases with unproven isolation conservatively instead of rotating everything and still not knowing why.
Customer communication should use artifact lineage as its unit. List the version, image digest, release date, distribution channel, whether an affected job produced it, the replacement artifact, and the verification method. If the investigation establishes only dependency resolution, the wording stops at resolution. If a clean rebuild comparison produces identical output bytes, the report should still state that credentials and the build environment were reviewed separately. These distinctions preserve urgency without describing every downloader as having executed the second stage.
The close condition is therefore measurable. No buildable branch or active cache resolves the unauthorized objects. Every install event has an evidence grade and owner. Every reachable credential has been invalidated, reviewed, or shown not to have been delivered. Every affected output has been deleted, disproved, or replaced by a clean attested build. Release controls reject the divergence used in the incident. Only then does the three-hour registry event stop casting an indefinite shadow.
8 The missing commit was the first alarm, not an alibi
Return to the engineer at the beginning. The repository looked clean because the fixed Axios source was clean. The application smoke test passed because the injected package did not need to change Axios behavior. The later dependency listing could look harmless because the installer attempted to replace its own manifest. Every observation was locally plausible. The mistake would have been to let any one of them answer a question it was never capable of answering.
Git answered what maintainers committed. npm answered what object a client could receive. The lock answered what a project selected. The package manager and endpoint answered what ran. The identity provider answered what authority became available. The artifact store and deployment system answered what survived the runner. The incident becomes legible only when those records are placed on the same timeline.
The mismatch between registry and source is therefore a reusable detection pattern. A popular name is not a source identity. A semver string is not a release ceremony. A valid package signature or provenance statement is meaningful only in relation to the workflow and source it claims. A clean repository does not absolve a divergent delivery object; it supplies the baseline that makes the divergence visible.
8.1 Confidence rises through independent evidence
The fixed v1.14.0 commit and manifest, pre-incident publish workflow, OIDC repair, script policy, and npm lifecycle and lock semantics show how normal publication worked and what maintainers later changed. The removed objects' manifest differences, setup flow, cleanup behavior, platform paths, and hashes are carried by Microsoft, the maintainer, Azure Pipelines guidance, and retained-artifact research. SOSEC did not obtain or execute those removed tarballs, so a local case still needs its own lock, process, network, file, and identity records to move from “source-described mechanism” to confirmed execution. Microsoft's Sapphire Sleet attribution can inform intelligence analysis, while runner isolation, reachable-credential response, and artifact rebuilding should follow directly from the delivered object and local evidence.
8.2 The durable control is a release story that cannot skip a page
The repaired system asks every release to tell the same story in several languages: a protected source tag, a fixed dependency graph, a known build workflow, a short-lived publishing identity, a reviewed environment, provenance, and a registry object whose bytes agree with those claims. Consumers add their own chapter by pinning the object, restricting installation code and egress, recording the build, and attaching outputs to that lineage.
No one witness has to be perfect for the system to be useful. The value comes from disagreement becoming loud. A registry package without a tag, a provenance claim without the expected workflow, a lock change without review, a new lifecycle hook without approval, or an install process reaching an unknown host all interrupt the story before the missing pages become an incident report.
Teams can adopt this model in stages. First, retain locks, registry receipts, job identities, and artifact digests long enough to join them. Next, enforce frozen resolution and make lifecycle execution visible. Then bind publication to a trusted workflow and require provenance at consumption. Finally, connect signing and deployment decisions to the same lineage. Each stage removes ambiguity even before the entire estate reaches the final design.
Legacy projects need a migration path; a permanent exemption would preserve the weakest release route. A repository that cannot yet use trusted publishing can still require a protected tag, a dedicated release identity, independent approval, immutable logs, and an after-publication comparison of the tarball against the expected source build. A package that genuinely requires install scripts can isolate that build step, remove release credentials, restrict egress, and attest the generated files before they enter the main graph. The objective is to name and constrain the remaining trust, keeping compatibility from hiding it.
Consumers should also decide what happens when corroboration is temporarily unavailable. A provenance service outage, registry metadata delay, or internal mirror failure should not silently downgrade policy to “accept anything with the right name.” High-risk release pipelines can fail closed or hold the object in quarantine. Less critical development workflows may permit an explicit, time-limited exception whose requester, reason, object digest, and later review are recorded. The exception becomes another visible witness in the release record.
Control performance should be measurable: builds use reviewed locks, unapproved lifecycle hooks are blocked, registry–source mismatches reach quarantine quickly, release artifacts carry verifiable provenance, and cache or deployment records trace back to the consuming job and source. Platforms can carry the daily burden through frozen installs, script admission, trusted mirrors, narrow runner identities, network policy, retained receipts, and release templates that obtain short-lived authority only after approval. Harmless tagless test packages, no-op lifecycle declarations, or test-only denied destinations can then exercise stopping, alerting, evidence retention, and authority isolation. An exercise that yields only a dashboard count without joinable receipts has not prepared the response chain.
That is the final reversal in the Axios case. At first, the absence of a malicious Git commit seemed reassuring. By the end, it is the clearest reason to stop: the software being delivered has no place in the source history that is supposed to authorize it. The engineer should not have to choose between trusting Git and trusting npm. The delivery system should require them to agree, preserve the receipts, and refuse to build when they do not.
Research record
9Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
9.1Searchable observables
Values copied from the cited material or recovered during this investigation, with the context needed to use them.
| Type | Value | Context | Action |
|---|---|---|---|
| Domain | sfrclak.com | Published historical infrastructure; correlate with time and process ancestry | |
| IP address | 142.11.206.73 | Published historical address on TCP 8000; hosting ownership can change | |
| SHA-256 | 92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a | Published macOS second-stage hash | |
| SHA-256 | ed8560c1ac7ceb6983ba995124d5917dc1a00288912387a6389296637d5f815c | Published Windows PowerShell-stage hash | |
| SHA-256 | f7d335205b8d7b20208fb3ef93ee6dc817905dc3ae0c10a0b164f4e7d07121cd | Published Windows batch-stage hash | |
| SHA-256 | fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf | Published Linux Python-stage hash |
9.2Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
Unauthorized 1.x registry release; removed from npm
Unauthorized 0.x registry release; removed from npm
Dependency carrying the reported postinstall delivery chain
9.3Event chronology
- Clean-looking dependency version seeded
[email protected] established an earlier publication record.
- Lifecycle delivery version published
[email protected] introduced the reported postinstall path.
- Two Axios release lines diverged
Unauthorized 1.14.1 and 0.30.4 registry objects referenced the injected dependency.
- Unauthorized Axios releases removed
npm removed both releases; the dependency followed at about 03:29 UTC.
- Release and installation controls hardened
Axios adopted trusted publishing, tightened workflow authority, added approval controls, and disabled dependency lifecycle scripts by project policy.
- SOSEC evidence review completed
SOSEC fixed the clean source objects, separated retained-artifact claims, and rebuilt the response around install-event lineage.
9.4Sources and material
- Microsoft Threat Intelligence: Axios npm supply-chain analysishttps://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/
- Axios maintainer postmortem and incident timelinehttps://github.com/axios/axios/issues/10636
- Microsoft Azure Pipelines customer guidancehttps://devblogs.microsoft.com/devops/axios-npm-supply-chain-compromise-guidance-for-azure-pipelines-customers/
- Axios v1.14.0 fixed clean source revisionhttps://github.com/axios/axios/tree/46bee3dea75ef53a8eae49f3b7487e6341de6074
- Axios clean manifest at the fixed revisionhttps://github.com/axios/axios/blob/46bee3dea75ef53a8eae49f3b7487e6341de6074/package.json
- Axios pre-incident publishing workflowhttps://github.com/axios/axios/blob/46bee3dea75ef53a8eae49f3b7487e6341de6074/.github/workflows/publish.yml
- Axios trusted-publishing migration commithttps://github.com/axios/axios/commit/ebf30369328b61c7ea9d152d4d5afa7d26e59c06
- Axios project install-script policy commithttps://github.com/axios/axios/commit/302e2739c602f00e323d4f3f5c79500647633a73
- Axios current source and release threat modelhttps://github.com/axios/axios/blob/v1.x/THREATMODEL.md
- Axios official release historyhttps://github.com/axios/axios/releases
- npm ci behavior and frozen lock contracthttps://docs.npmjs.com/cli/commands/npm-ci/
- npm lifecycle-script documentationhttps://docs.npmjs.com/cli/using-npm/scripts/
- npm configuration and ignore-scripts policyhttps://docs.npmjs.com/cli/configuring-npm/npmrc/
- npm trusted publishinghttps://docs.npmjs.com/trusted-publishers/
- npm package-lock semanticshttps://docs.npmjs.com/cli/configuring-npm/package-lock-json/
- npm provenance statementshttps://docs.npmjs.com/generating-provenance-statements/
- StepSecurity retained-artifact analysis of the removed packageshttps://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan