Home - Coinspect Security
pin dependency

Evolving Supply Chain Attacks: Why dApps Avoided a Major Breach

Senior Security Consultant
Ethereum, DeFi

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

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 19:59:00 UTC — using our dApp frontend monitoring system. 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 the deprecation warning string “Instance method debug.destroy() is deprecated and no longer does anything. It will be removed in the next major version of debug.”

During the attack window, our monitoring 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.”
↓ @metamask/utils/dist/logging.js requires debug
↓ debug/src/browser.js ends up in user browsers

Dev Dependencies vs. Production Dependencies

Many packages across the ecosystem also depend on debug — tools like eslint-import-resolver-typescript or vitest declare it as well. But those are dev-only and never make it into production. The challenge is that identifying what actually ships to production isn’t always straightforward in dApp frontends. For Curve, only the @metamask/utils → debug path made it into the bundle, and reaching that conclusion required dedicated tooling.

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 would have allowed the malicious debug@4.4.2 release. The yarn.lock file overrode them all, forcing the installer to resolve to debug@4.4.1. That single constraint kept the compromised version out of production

However, the yarn.lock file forced all these different ranges to resolve to a single version:

“debug@npm:^4.1.0, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.1”:
version: 4.4.1

This case demonstrates with certainty that dependency pinning is not optional hygiene, but a critical security control. Curve Finance was only protected because its lockfile enforced debug@4.4.1 and blocked the compromised debug@4.4.2 release. Without strict pinning, the malicious payload would have executed in production browsers of 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 widgets 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

Don’t trust semantic versioning 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.

Review the latest documentation for your package manager—many now include security-oriented settings that can further reduce risk. For example, pnpm supports configuring a minimum package age, a simple but practical safeguard against some of the most common supply chain attacks.

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.