Home - Coinspect Security
pin dependency

Evolving Supply Chain Attacks: Why dApps Avoided a Major Breach

Senior Security Consultant
Supply chain

In the past three weeks, we have seen a string of escalating software supply chain attacks targeting NPM packages. Last week, attackers phished Qix—a trusted JavaScript libraries maintainer—and used the compromise to slip malicious code into popular NPM packages with a combined 2.6 billion weekly downloads.

In this post, we analyze the Qix breach window and trace how wallet-stealing malware nearly cascaded into the broader crypto ecosystem. In an upcoming follow-up, we’ll step back to examine all three recent attack waves and share our perspective on how teams can prepare for the next one.

The attack didn’t succeed. Some dismissed it, even mocked the attackers, and moved on. That’s a mistake. We can’t judge these events only by their outcomes. What matters is what the attackers attempted, why they failed, and what small differences might have made the attack work. Were they targeting specific projects? What evidence do we have? How do we harden those projects?

How Web3 Frontends Stayed Safe

In this case, the attackers slipped malicious code into popular NPM packages with the goal of compromising dApp frontends and draining user wallets. Unlike broader supply chain campaigns (e.g., S1ngularity and Shai-Hulud) that target CI/CD pipelines and local developer environments, this payload was narrowly crafted to run only in the browser. The attempt didn’t spread widely, but it highlighted just how exposed frontends remain to dependency-level attacks.

Using our unique dApp frontend monitoring system we analyzed the incident window from the Qix phishing attack — beginning with the publication of the first compromised package, ansi-styles@6.2.2, on 2025-09-08 13:12:10 UTC, until NPM confirmed the removal of all malicious versions at 2025-09-08 19:59:00 UTC. This allowed us to review deployments made during the attack window and confirm that no production builds included the malicious code.

One of the compromised libraries was debug, a ubiquitous logging utility. Our monitoring flagged its presence in production bundles through simple fingerprinting.

During the attack window, our monitoring system detected more than 30 new frontend deployments that shipped debug in their production bundles, covering major dApps like Curve, Lido, Metamask Portfolio, GMX, the Arbitrum Bridge, and Jupiter. All of these dApps were pinned to safe versions. Without dependency locking, they would have silently pulled the compromised release and shipped the attacker’s payload directly into user browsers.

Case Study: Curve Finance

To illustrate how dependency pinning saved a real application, we analyzed the Curve Finance frontend, a major DeFi protocol handling billions in TVL.

We focused exclusively on dependencies that end up in the production bundle served to users, not development tools. Using bundle analysis (via rollup-plugin-visualizer), we traced which specific code of debug actually reached the end user’s web browsers.

When Indirect Dependencies Hit Production

Debug isn’t an exotic library, it includes a browser module, so its presence in frontends is expected. What stands out is the path it takes. Our analysis traced a dependency chain that starts with a minor logging utility buried in a wallet SDK and ultimately lands in user browsers.

In Curve’s case, that path runs six layers deep through indirect dependencies. No one added debug directly, yet the chain still drags it into production.

Curve Frontend
↓ imports curve-ui-kit
↓ imports @wagmi/connectors
↓ depends on cbw-sdk@3.9.3 (Coinbase Wallet integration)
↓ depends on eth-block-tracker@7.1.0
↓ depends on @metamask/utils@5.0.1
↓ depends on “debug”: “^4.3.4”
↓ @metamask/utils/dist/logging.js requires debug
↓ debug/src/browser.js ends up in user browsers

Which Version Reaches Production

Many packages across the ecosystem also declare debug — tools like eslint-import-resolver-typescript or vitest list it as a devDependency. Those never ship to user browsers, though if compromised they could still expose CI pipelines or developer machines.

The challenge is tracing which version of a library actually lands in the production bundle. That depends on lockfile resolution and bundling behavior. In Curve’s case, the relevant path to production ran several layers deep, so we confirmed it through bundle analysis, since it wasn’t visible from package.json alone.

From an attacker’s view, an unpinned production dependency is the fastest path to client-side execution: no CI pivot, no stolen tokens — just one poisoned release away from users. That route is low-friction and high-reward for attackers. In Web3 the danger multiplies because teams share stacks (wallet connectors, SDKs, block trackers); compromise one widely used integration and the payload can ripple across many dApps.

Why the Lockfile Saved Curve

Package.json defines version ranges, while yarn.lock pins the exact versions that actually get installed. That distinction is why the attack failed.

Across Curve’s dependency tree, several packages requested the debug library with different ranges:

  • @metamask/utils: "debug": "^4.3.4" (production dependency - ships to users)
  • eslint-import-resolver-typescript: "debug": "^4.4.1" (dev dependency - build tools only)
  • vitest: "debug": "^4.4.1" (dev dependency - testing only)
  • Others: Various ranges like ^4.3.2, ^4.1.0, etc.

All of these ranges could have accepted the malicious debug 4.4.2 release.

In Curve’s yarn.lock, the ranges that feed the production path to debug were already pinned. Even if other subtrees requested different ranges, the build we analyzed resolved the production bundle to 4.4.1, not 4.4.2.

As a result, when the malicious 4.4.2 was published and Curve shipped a new release of its frontend during the attack window, the install stayed on the safe version. In this case, the use of lockfiles was a good practice that directly prevented attacker code from reaching one of DeFi’s largest frontends.

Something to Watch: Supply Chain Through Widgets

Several major dApps embed the Intercom widget directly from a CDN, including 1inch, dYdX, Ether.fi, and Symbiosis Finance. Intercom itself depends on the debug library, one of the packages affected during the Qix attack. If Intercom had rebuilt its widget during the attack window, pulling in a poisoned release, every one of those frontends would have served the attacker’s payload to users instantly — without any action by the dApp teams themselves.

This type of widget is just one example of dynamic third-party JavaScript dependencies that can be compromised to attack users of legitimate dApps. We have an ongoing research project about this issue; check the dApp Observatory to explore the dynamic dependencies of major dApps.

Lessons for Web3 Builders

During the Qix phishing incident, popular web3 frontends nearly shipped a compromised update. Debug, a ubiquitous logging utility, showed up in more than 30 production releases from major dApps. Those builds stayed safe only because lockfiles pinned them to trusted versions.

Lock and Verify Your Dependencies

Semantic version ranges (^, ~) are convenient for feature upgrades—but dangerous for security. Always use lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) to ensure you’re running the exact, vetted versions of your dependencies. This prevents unexpected upgrades from silently introducing malicious or unreviewed code.

Make sure you:

  • Audit lockfile changes: Treat dependency updates like code changes. Every new version should be reviewed and approved.

  • Use CI-safe installs: Run npm ci, yarn install --frozen-lockfile, or pnpm install --frozen-lockfile in your pipelines to enforce lockfile consistency.

  • Leverage package manager features: For example, pnpm supports minimum package age, delaying installs until new releases have been public long enough to detect malicious activity.

Prepare for Evolving Attacks

This incident is a warning. Here are additional immediate actions every development team should take:

  • Mandate Phishing-Resistant MFA. Move your entire team to hardware security keys (like YubiKey) for all critical accounts (GitHub, package repos, cloud providers).
  • Assume Upstream Compromise. Build with a zero-trust mindset. Protect secrets from potential CI/CD environment compromise. Use public-key based authentication when available. Use sandboxing and strict permissions to limit the “blast radius” of a compromised dependency.
  • Practice Incident Transparency. When something goes wrong in a project you maintain, disclose it quickly, clearly, and responsibly. A fast, honest disclosure helps the entire ecosystem contain the damage.

Complacency is dangerous. Every failed attack is research for the next one. Adversaries learn, adapt, and improve. If we only measure success by the amount of funds stolen, we underestimate the threat. Attacks don’t get weaker over time—they get better.