Why bank change control never saw the npm hijack

In September 2025, poisoned versions of widely used open-source code were live for about two hours. Bank change control was built to approve the code people write. This attack came in through code the build process fetched by itself, which nobody approves.

Looking Back revisits one week at a time, from January 2020 to today, and asks what it meant for the banks, health systems and public agencies we serve. The series was written in 2026, with the benefit of hindsight, and each article says which week it looks back on.

Two-minute summaryThe dek and the first sentence of each section, quoted from the article

In September 2025, poisoned versions of widely used open-source code were live for about two hours. Bank change control was built to approve the code people write. This attack came in through code the build process fetched by itself, which nobody approves.

  1. The week Almost every modern website and app is assembled, not written from scratch.
  2. How modern software is really built Most executives think of software as something a team writes.
  3. What change control actually reviews A typical online banking front end pulls in hundreds or thousands of open-source packages, most of them several layers removed from anything a developer chose.
  4. Time as a control The September 8 versions were removed within about two hours.
  5. The packages a bank publishes Banks and fintechs are also publishers.
  6. Protection at the moment of payment Even with all three controls, some malicious code will eventually reach a browser, whether through a dependency, a third-party script or an extension on the customer’s device.
  7. Four routes to the executive suite Software supply chain risk can sound like a developer’s problem.
  8. Four owners and one test A mature program here is not exotic.
  9. With hindsight The September 8 compromise was contained quickly, but Shai-Hulud spread further.

The week

Almost every modern website and app is assembled, not written from scratch. Developers pull in small, freely shared building blocks of code, called packages, from public libraries known as registries. The biggest registry for JavaScript, the language that runs inside web browsers, is npm. A single popular package can be included in thousands of other packages, which are in turn included in millions of websites. Most of the people relying on it have never heard its name.

On September 8, 2025, that hidden plumbing was turned against its users. Attackers published malicious versions of about 18 popular npm packages, including two called chalk and debug. They got in the simplest way possible: they phished the maintainer, the developer who looks after those packages, with a fake two-factor authentication reset email sent from a look-alike domain, npmjs.help. Once they had the maintainer’s account, they could publish new versions under a trusted name.

Security firms tracking the incident put the packages’ combined downloads at more than two billion a week, mostly as indirect dependencies. An indirect dependency is a package that a developer never chose directly; it came along because something they did choose needed it.

The injected code ran in web browsers. It hooked network requests and wallet interfaces, meaning it silently listened to the messages a web page sent and to the software people use to hold cryptocurrency, and swapped cryptocurrency destination addresses for the attackers’ own. A customer who thought they were sending funds to one address could have been sending them to the thieves instead.

The response was fast. Security researchers flagged the change quickly, and the maintainer and npm removed the malicious versions within about two hours, according to GitHub’s later account.

The week was not over as a story, though. A week later a separate self-propagating campaign, later named Shai-Hulud, began stealing developer tokens, the digital keys that let a developer publish packages, and using them to publish infected versions of hundreds more packages. Each stolen key opened the door to more packages, and each infected package could steal more keys.

Take a bank’s change advisory board on a Tuesday morning. On the agenda is a small update to the online banking site: a new button, a fix to a date field. The engineer walks through the code changes line by line. The risk is rated low, a release window is approved for that evening, and everyone moves on. Every box has been ticked.

Then suppose that, at the moment the build runs that evening, the software that assembles the website reaches out to the public npm registry and fetches the latest version of a tiny utility package buried several layers deep. If that evening happened to fall inside the two hours on September 8, 2025, the bank could have shipped attacker code to its customers inside an approved, low-risk change. Nobody on the board would have seen it, because it was never in front of them.

The payload targeted cryptocurrency wallets, so most banks read the news as someone else’s problem. The delivery mechanism was the part that applied to them. Bank change control would not have stopped it. A change advisory board reviews a diff written by an engineer and approves a release window. The dependency tree that the build tool resolves at that moment is not in the diff, and no one signs off on it.

The gap has four parts: what change control actually reviews, time as a control, the packages a bank publishes, and protection at the moment of payment.

How modern software is really built

Most executives think of software as something a team writes. In practice, a team writes a thin layer of code that is specific to the business, and stands it on top of a great deal of code written by strangers. That borrowed code is open source: published freely, maintained largely by volunteers and small teams, and reused across the world.

A few terms help. A package is a bundle of reusable code that does one job, such as coloring text, formatting dates or logging messages. A dependency is a package your software needs in order to work. A dependency tree is the full family tree: the packages you chose, the packages they rely on, and so on down several generations. A registry, such as npm, is the public warehouse those packages are fetched from.

A build is the automated process that takes the developers’ code, fetches all the dependencies and assembles a finished application. A diff is the record of exactly which lines a developer changed, and it is what reviewers normally read.

The weak point sits between the last two. Unless told otherwise, many build tools will happily fetch the newest acceptable version of each dependency at the moment they run. That means the finished application can contain code that did not exist when the change was reviewed, chosen by no one, from an author no one at the bank has met.

What change control actually reviews

A typical online banking front end pulls in hundreds or thousands of open-source packages, most of them several layers removed from anything a developer chose. Change records describe features and fixes. They rarely list the third-party code that changed between two releases, because nobody writes that code and the tooling hides it.

Closing this gap starts with making dependency changes visible as changes. Builds should run from a committed lock file using a clean-install command that refuses to resolve new versions, so the dependency set changes only when someone edits the lock file. Each edit then appears in the pull request, where a reviewer can see that a utility package moved versions and why.

A lock file is a precise shopping list. Rather than saying “a recent version of chalk,” it records the exact version of every package in the tree, down to the last indirect dependency. Committing it means storing it alongside the code, under the same review. A clean install is a build command that follows the shopping list exactly and stops if anything does not match. A pull request is the standard way a developer proposes a change for review.

A software bill of materials generated per release, and stored with the change record, lets an incident team answer within minutes whether a named version ever shipped. The name is borrowed from manufacturing, where a bill of materials lists every component in a product. For software, it is the list of every package and version inside a release. When an advisory says “version X of package Y was malicious,” the bank can search its records rather than its memory.

Classification matters as well. Many banks treat front-end releases as lower risk than core payment changes. In a browser-based payment flow, the front end decides which account number travels to the server. It deserves the same change category as the payment service behind it. The “front end” is the part of the application that runs on the customer’s own screen; the “server” is the bank’s computer that receives the instruction. If the screen is lying, the server receives a perfectly valid instruction to send money to the wrong place.

Time as a control

The September 8 versions were removed within about two hours. That window is typical for loud registry compromises, which are often caught quickly by researchers who scan new releases. An institution that waits a few days before adopting any new version avoids most of them at almost no cost.

Most malicious releases are spotted and pulled quickly, so a short wait before adopting a brand-new version sidesteps most of them, at the price of running slightly older code.

Tooling caught up with the idea in 2025. GitHub’s Dependabot, a service that automatically proposes dependency updates, added a cooldown setting in July 2025 that delays update proposals for new releases, and the pnpm package manager added a minimum release age setting that September. Internal package proxies can enforce the same rule centrally by refusing versions younger than a set age. A package proxy is an in-house middleman between the bank’s builds and the public registry; every package passes through it, so one rule there applies to every team.

The trade-off is security patches. A blanket delay also slows urgent fixes. The workable policy is a default wait with a documented exception path, where the security team can approve an urgent version after checking it. That keeps the decision with a named person rather than with whichever build ran first.

Delay has limits. It works against compromises that are noisy and quickly reported. It does little against a patient attacker whose code stays dormant for weeks, or against a package that was malicious from its first release. Those cases still depend on scanning new versions in the proxy and on an inventory precise enough to find affected releases once an advisory appears.

The packages a bank publishes

Banks and fintechs are also publishers. Many maintain software development kits, widgets and shared component libraries, on public registries or internal ones. A software development kit, or SDK, is a package a bank gives to partners or customers so their software can connect to the bank’s services easily. Shai-Hulud showed how that role becomes an attack path. It stole tokens from developer machines and build systems, then used them to publish infected versions of whatever packages those tokens could reach.

Controls here look like privileged access management, the discipline banks already apply to administrator passwords and powerful system accounts. Long-lived publishing tokens should be retired in favor of trusted publishing, where the build system obtains a short-lived credential tied to a specific repository and workflow. Instead of a permanent key that works from anywhere, the credential is valid only for one approved process and only for a short time, so stealing it is of little use.

Publishing should require phishing-resistant authentication, such as hardware security keys that will not work on a look-alike website, which is precisely the trick used against the chalk and debug maintainer. Developer workstations and build runners should hold no standing registry credentials. A compromised bank SDK would reach partners and customers under the bank’s own name, which turns a supply chain incident into a reputational one.

Protection at the moment of payment

Even with all three controls, some malicious code will eventually reach a browser, whether through a dependency, a third-party script or an extension on the customer’s device. The last layer has to assume that.

Content Security Policy and Subresource Integrity limit which scripts a page can load and detect tampering with fixed third-party files. Content Security Policy is a list the website hands the browser of the sources of code allowed to run; Subresource Integrity is a fingerprint of a known file that the browser checks before using it. Neither helps if the malicious code is already inside the bank’s own approved bundle, which is why the earlier controls matter.

Server-side checks catch what the browser hides: payments to a never-before-seen destination, or a payee whose name does not match the account. Out-of-band confirmation, such as a push notification on a registered device that shows the payee, gives the customer a view the compromised page cannot alter. “Out of band” means through a separate channel, the way a bank might call you on a known number to confirm a large wire. These controls are familiar from fraud programs. The npm incident is a reason to treat them as protection against software supply chain failures as well.

Card industry rules point the same way. PCI DSS version 4.0 requirements for inventorying and monitoring scripts on payment pages became mandatory on March 31, 2025. PCI DSS is the security standard the card networks require of anyone handling card data. The requirements apply to entities handling card data, and they are a reasonable model for any payment page a bank runs.

Four routes to the executive suite

Software supply chain risk can sound like a developer’s problem. It reaches the executive suite in four ways. Customers who lose money through a bank’s own website will not care whose code was at fault. Examiners and auditors increasingly ask how institutions manage third-party and open-source software. Incident response depends on answering “did we ship it?” quickly, which is impossible without records. And the cost of these controls is small compared with the cost of rebuilding trust.

Suppose two regional banks hear the news on the afternoon of September 8. The first builds from lock files, keeps a bill of materials for every release and applies a waiting period to new versions. Within an hour its team confirms that no affected version was ever pulled in and tells the board so in writing. The second has none of this. Its engineers spend days reconstructing which builds ran when and what they fetched, while the communications team waits to know what to tell customers. Both banks may turn out to be unaffected. Only one can prove it quickly.

The ways banks get this wrong are predictable: treating open-source code as free in every sense, rating front-end changes as low risk by default, leaving permanent publishing keys on laptops, and assuming that because a payment instruction arrived intact at the server, it reflects what the customer intended.

Four owners and one test

A mature program here is not exotic. It is a set of ordinary disciplines applied to a part of the software that used to be invisible. Each has a natural owner, and much of the failure in this area comes from nobody being sure whose job it is.

Engineering leadership owns the build. Its job is to make sure that every customer-facing application builds from a committed lock file with a clean-install command, that a bill of materials is produced for every release, and that the default waiting period for new package versions is switched on in the tooling rather than left to individual teams to remember.

The change management function owns the classification. Its job is to make sure dependency changes are recorded as changes, that the bill of materials is attached to the change record, and that payment-page releases sit in the right risk category. This is often a small edit to an existing policy, not a new process.

Security owns the exceptions and the publishing controls. A named person decides when an urgent version can skip the waiting period, after checking it. The same team makes sure that any packages the bank publishes use trusted publishing, that publishing accounts use phishing-resistant authentication, and that no permanent registry keys sit on laptops or build systems.

Fraud and payments operations own the last layer. They already run payee checks and confirmations for fraud reasons; the task is to recognize that these same controls protect customers when the bank’s own web page has been tampered with, and to keep them in place for higher-risk payments even when customers ask for fewer steps.

A simple measure shows whether all of this is working: the time it takes the bank to answer, with evidence, “did any of our releases contain the affected versions?” If that answer takes minutes, the disciplines are in place. If it takes days of reconstruction, they are not, whatever the policies say.

It also helps to rehearse. A short tabletop exercise, in which the team is handed a mock advisory naming a compromised package and asked to report back to an executive within the hour, will reveal more about readiness than any policy review. The gaps it exposes are usually mundane: a build that does not keep records, a team that never adopted the lock file, a publishing key nobody knew existed.

Lock files, release inventories, publishing keys and payment checks

  • Do our customer-facing applications build from a committed lock file, so that every dependency change shows up in a reviewed pull request?
  • For any given release, can we produce a list of every package and version it contained, and how long would that take?
  • Do we wait before adopting brand-new package versions, who can approve an urgent exception, and are front-end changes to payment pages classified at the same risk level as the payment services behind them?
  • If we publish SDKs or libraries, how are they published, and are there long-lived publishing keys on developer machines or build systems?
  • What server-side checks and out-of-band confirmations protect higher-risk payments if the customer’s screen has been tampered with, and how do our payment pages measure up against the PCI DSS 4.0 script requirements?

With hindsight

The September 8 compromise was contained quickly, but Shai-Hulud spread further. CISA issued an alert on September 23, 2025 describing a worm that had compromised more than 500 packages. A worm is malicious software that spreads by itself from one victim to the next, without a person directing each step, and this one used each stolen publishing key to reach new packages.

A day before CISA’s alert, GitHub had announced planned changes to npm publishing. They included mandatory two-factor authentication for local publishing, a seven-day maximum lifetime for granular publishing tokens, deprecation of classic tokens, and wider trusted publishing. Each of those changes narrows the opening the attackers used: a phished password is less useful with two-factor authentication, a short-lived key is less useful once stolen, and trusted publishing removes many standing keys altogether.

A second wave, which GitHub described as Shai-Hulud 2.0, followed in late 2025 and focused on build environments, the automated systems that assemble and publish software. In July 2026, GitHub made a three-day cooldown the default for Dependabot’s non-security version updates, citing the September 2025 incident. An idea that was an optional setting in mid-2025 had become the default a year later.

The incidents showed that registry hygiene improves slowly and unevenly. The public ecosystem did get safer, but no bank can wait for every volunteer maintainer to adopt better habits. The controls that mattered most were the ones banks could apply to their own builds.

Banks that pinned dependencies, delayed adoption and recorded what each release contained could answer customer and examiner questions in hours. That is the useful test of any supply chain program: not whether it promises to stop every attack, but whether, on the afternoon the next advisory lands, the institution can say quickly and confidently whether it was affected.

Spotted an error? Tell us and we will check it and correct the article where needed.

  • npm
  • software supply chain
  • change management
  • web applications
  • payments fraud