npm installpostinstall hook.Example:
plain-crypto-js@4.2.1 (axios)--ignore-scripts, pnpm 10 and npm v12 defaultsrequire() / importExample:
@joyfill/layouts betalogger.trace()Example: Contagious Interview cluster
A remote access trojan (RAT) is malware disguised as legitimate software that gives an attacker persistent, interactive control of the machine running it.
Also known as RATs, remote access trojans, remote administration trojan, RAT malware, remote access trojan malware
A remote access trojan (RAT), also called a remote administration trojan, is malware that hides inside software a victim chooses to run, then opens an outbound command channel so an attacker can operate the machine: run shell commands, read and upload files, and install more payloads. Developers now receive RATs through npm and PyPI packages.
A remote access trojan is a remote administration tool that the machine's owner never agreed to run. The "trojan" half describes delivery: the code arrives inside something the victim wants, such as a cracked game, a meeting app, or a logging library. The "remote access" half describes what it does once running: it registers the machine with attacker infrastructure and waits for instructions.
That waiting loop separates a RAT from its neighbors. An infostealer grabs credentials and exits. A backdoor is a hidden way in. A RAT is a full agent with a command vocabulary, and the operator decides what to do after looking around the machine. SSH and TeamViewer do the same job with the owner's consent and a visible session. A RAT does it covertly.
The interactive session changes the incident response. After an infostealer runs, you rotate what it could read. After a RAT runs, the attacker could have done anything the user account could do, for as long as the session lived. The host is compromised, not just its secrets.
Our position: RATs are now the highest-impact payload in open source malware, because developer machines hold publish rights. The axios compromise is the clearest case. The axios post-mortem states the root cause directly: "The attacker gained access to the lead maintainer's PC through a targeted social engineering campaign and RAT malware." The attacker then used that access to publish axios versions that installed a second RAT on every machine that pulled them. Axios has about 100 million weekly downloads on npm.
A RAT dials out; it does not listen. Home routers, corporate NAT, and cloud security groups drop unsolicited inbound connections, but nearly every network allows outbound HTTPS. So the implant starts every exchange. It sends a check-in, called a beacon, to its command and control (C2) server. The response carries the next command, and the next beacon carries the result.
The beacon interval is the main design trade-off. A short interval gives the operator a responsive shell but leaves a regular, detectable rhythm in network logs. The discordpydebug RAT on PyPI polled its server every second and reached more than 11,000 downloads. The macOS RAT in the axios attack beaconed every 60 seconds. The RAT recovered from the Joyfill compromise skipped polling and held an open Socket.IO connection, so its operator could push commands instantly.
Every RAT ships a command table, and the table tells you what the operator can do. Our analysis of the axios macOS second stage, a C++ binary, recovered four commands:
peinject receive a Base64 binary, write it to a hidden random file in /private/tmp, ad-hoc codesign it, execute it
runscript run a shell command via /bin/sh, or a Base64 AppleScript via /usr/bin/osascript
rundir enumerate a directory: names, sizes, timestamps, hierarchy
kill terminate the implantFour commands is plenty. runscript is arbitrary code execution, and peinject installs anything else. Google's Threat Intelligence Group named this implant WAVESHAPER.V2, called it "a fully functional RAT," and attributed the axios attack to UNC1069, a North Korea-nexus group active since 2018.
Package RATs are almost always staged. The code in the package is a small loader, and the RAT is fetched at run time. The axios dropper was a 4,209-byte setup.js. The Joyfill loader looked up a Tron or Aptos account, read an encrypted payload from a BNB Smart Chain transaction, and repeated the trick once more before reaching a 77,276-byte Node.js RAT. Staging keeps the published artifact small and lets the operator swap payloads without republishing. It also means the copy on the registry often contains no RAT at all, only instructions for fetching one.
Remote access trojans get into npm and PyPI packages in three ways: attackers publish lookalike packages, take over a real maintainer's account, or inject code into a legitimate build. For defenders, the entry route matters less than the trigger, the moment in a package's life when the loader first runs.
npm installpostinstall hook.plain-crypto-js@4.2.1 (axios)--ignore-scripts, pnpm 10 and npm v12 defaultsrequire() / import@joyfill/layouts betalogger.trace()There are three triggers, and each one defeats a different control.
Install time. The loader runs from an install script such as postinstall. The axios attack used this trigger: plain-crypto-js@4.2.1 ran node setup.js from its postinstall hook. npm install --ignore-scripts, the pnpm 10 default, and the npm v12 allowScripts default all block it.
Import time. The loader runs as top-level module code the first time anything calls require() or import. Install-script controls never see it. The Joyfill beta releases of July 28, 2026, used this trigger, 20 days after npm v12 shipped on July 8, 2026, with install scripts off by default. Our Joyfill write-up is blunt: "npm install --ignore-scripts does not help here."
Call time. The loader sits behind an ordinary-looking function and runs only when the application calls it. The Contagious Interview campaign, which Socket had linked to more than 1,700 malicious packages as of April 2026, hid loaders in a Rust logger's trace() method and a Python license helper's find_by_key() in one five-ecosystem cluster. Neither install-script blocking nor reading module entry points catches this.
Only checks that run before the package lands apply to all three triggers: analyzing the package's code, and refusing known-malicious versions at the point of install.
You can watch an install-time control miss import-time code with a harmless local package:
mkdir demo-pkg app
cat > demo-pkg/package.json <<'EOF'
{ "name": "demo-pkg", "version": "1.0.0", "main": "index.js",
"scripts": { "postinstall": "node -e \"console.log('[postinstall] ran')\"" } }
EOF
echo "console.log('[import] demo-pkg top-level code ran');" > demo-pkg/index.js
(cd demo-pkg && npm pack --silent)
cd app && npm init -y >/dev/null
npm install --ignore-scripts ../demo-pkg/demo-pkg-1.0.0.tgz
node -e 'require("demo-pkg")'added 1 package, and audited 2 packages in 452ms
[import] demo-pkg top-level code ranThe postinstall line never prints. The import line always does. Replace the console.log with fetch-and-eval and you have the execution model of the Joyfill loader. Default-on install scripts were a design mistake, and turning them off is now table stakes. Attackers have already moved to the next trigger.
We compared six RAT deliveries that our research team has published on, from 2024 to 2026. They include a commodity RAT, Quasar RAT disguised as an npm package, and a targeted one, the npm cluster aimed at Alibaba developers. The table lists how each one ran, how it talked home, and where it hid.
| Package (ecosystem, date) | Trigger | Channel | Capability highlights | Persistence |
|---|---|---|---|---|
| ethereumvulncontracthandler (npm, Dec 2024) | Install | Quasar RAT C2 on port 7000 | Keylogging, screenshots, credential theft | Registry Run key, binary renamed client.exe |
| discordpydebug (PyPI, published 2022, reported May 2025) | Call: run() and debug() | HTTP polling every 1 s | readfile, writefile, shell | None |
| plain-crypto-js via axios 1.14.1 and 0.30.4 (npm, Mar 2026) | Install: postinstall | HTTP POST beacon every 60 s | peinject, runscript, rundir, kill | /Library/Caches/com.apple.act.mond on macOS |
| license-utils-kit and siblings (PyPI, npm, Go, crates.io, Packagist; Apr 2026) | Call: logger and helper methods | Server returns a download URL for a staged ZIP | Remote shell, keylogging, browser and wallet theft | Not detailed; payloads named com.apple.systemevents and systemd-resolved |
| @joyfill/layouts 0.1.2-2773.beta.0 (npm, Jul 2026) | Import: module load | Socket.IO; stages resolved through blockchain transactions | ss_eval, ss_upf, ss_cb (clipboard), shell | Code injected into VS Code, Cursor, Discord, GitHub Desktop, and the global npm CLI |
| local-config-parser and smart-config-manager cluster (npm, published Apr 2026, reported Jul 2026) | Load: auto-initialization code | Hardcoded C2 plus encrypted reverse TCP proxy | Unknown commands go to the OS shell; DingTalk lateral movement | Patches Python files in AI tools' .skills directories |
Three patterns stand out. At most two of the six ran at install time. The command sets are small, because shell access makes everything else redundant. And the two most recent RATs persist inside developer tools, not inside node_modules.
That last pattern is the one most teams miss. The Joyfill RAT inserts a self-reloading block into lib/cli.js of the global npm install, so it runs again every time someone types npm. Deleting the malicious package, regenerating the lockfile, and reinstalling leaves that file untouched.
The npm registry keeps publish timestamps after it removes a version. Querying the axios packument on October 30, 2026, still returns both malicious releases in its time map, though they are gone from versions:
npm view axios time --json | grep -E '"(0\.30\.[34]|1\.14\.[01])"'
npm view plain-crypto-js time --json "0.30.3": "2026-02-18T17:19:20.081Z",
"1.14.0": "2026-03-27T19:01:40.915Z",
"1.14.1": "2026-03-31T00:21:58.168Z",
"0.30.4": "2026-03-31T01:00:57.285Z",
"4.2.0": "2026-03-30T05:57:32.867Z",
"4.2.1": "2026-03-30T23:59:12.278Z",
"0.0.1-security": "2026-03-31T03:25:11.636Z",Combined with our detection log and the maintainer's post-mortem, those timestamps give the sequence:
plain-crypto-js@4.2.0 published. Clean copy of crypto-js, builds history.plain-crypto-js@4.2.1 published with a postinstall dropper.axios@1.14.1 published with plain-crypto-js added as a dependency.axios@0.30.4 published, poisoning the 0.x line 39 minutes later.plain-crypto-js with a security placeholder. Axios versions come down after about three hours live.time data, Socket's detection log, and the axios post-mortem. Times in UTC.| Time (UTC) | Event |
|---|---|
| Mar 30, 05:57:32 | plain-crypto-js@4.2.0 published: a clean copy of crypto-js |
| Mar 30, 23:59:12 | plain-crypto-js@4.2.1 published with a postinstall dropper |
| Mar 31, 00:05:41 | Socket flags plain-crypto-js@4.2.1 as malware |
| Mar 31, 00:21:58 | axios@1.14.1 published with plain-crypto-js as a new dependency |
| Mar 31, 01:00:57 | axios@0.30.4 published, 39 minutes later |
| Mar 31, 03:25:11 | npm replaces plain-crypto-js with a security placeholder; the axios versions came down after about three hours live |
The order matters. The attacker published a clean plain-crypto-js@4.2.0 18 hours early, so the name had a history. The malicious 4.2.1 went up 23 minutes before the first axios release. Socket's automated malware detection flagged 4.2.1 at 00:05:41 UTC, 16 minutes before axios@1.14.1 existed. Anyone who installed the bad axios releases was pulling a dependency already marked as malware. The malicious axios versions were live for about three hours, according to the post-mortem.
The dropper also covered its tracks. After launching the second stage, setup.js deleted itself and renamed a bundled package.md to package.json, restoring a clean 4.2.0 manifest with no postinstall field. Anyone who inspected node_modules/plain-crypto-js afterward saw an ordinary copy of crypto-js.
Exposure reached beyond projects that list axios as a dependency. Most packages declare axios with a caret range, so any fresh resolution during the window picked up 1.14.1. As we showed in the hidden blast radius of the axios compromise, tools run through npx, global installs, and new CI images resolved against the live registry, and none of that shows up in a project lockfile. A clean lockfile today is not evidence you were safe in March.
You detect a remote access trojan in a dependency at two points: before it runs, by reading what the package code does, and after it runs, by finding its beacon and its persistence. The second is the classic endpoint problem. The first exists only in the supply chain, and it is where we focus.
Before execution. A package-delivered RAT must do things in code that a logger or a crypto helper has no reason to do. It opens network connections to hardcoded hosts, spawns shells or child processes, decodes and evaluates remote content, and hides those strings behind obfuscation. Socket reports these behaviors as alerts, including Network access, Shell access, Uses eval, and Obfuscated code, and marks confirmed versions as Known malware. The limit: staged loaders keep the RAT off the registry, so analysis often sees only the loader, not the implant. A loader that reads its next stage from a blockchain transaction can look like ordinary web3 code until someone traces the decode-and-eval.
Blocking at install time turns a detection into protection. A package firewall such as Socket Firewall sits between the package manager and the registry and refuses artifacts already confirmed as malware, including transitive ones. It does not block versions nobody has analyzed yet, and it cannot block a tarball already in your local cache.
After execution. These indicators of compromise are worth hunting on hosts and in network logs:
node or python spawning curl, osascript, cscript, or a detached node -e. On Windows, the axios dropper copied powershell.exe to %PROGRAMDATA%\wt.exe to pass as Windows Terminal.# axios macOS second stage
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null
# Joyfill persistence markers injected into the global npm CLI
NPM_PKG=$(dirname "$(dirname "$(readlink -f "$(command -v npm)")")")
grep -lE '/\*(C2506(1[7-9]|20)A|C26051[12]A|RS260605)\*/' "$NPM_PKG/lib/cli.js" \
|| echo "no Joyfill marker in npm CLI"
# Alibaba-targeting cluster marker in Python files
grep -rl '# __INJECT_MARKER__' ~ --include='*.py' 2>/dev/null | headEach check catches one family and misses the next. The Joyfill RAT also refuses to run on hostnames such as github-runner and buildbot, so a sandbox that looks like CI may never see it execute.
A RAT that has run is an incident on the host, not a bug in a dependency. Treat every machine that installed or imported the package as compromised, and work in a fixed order. Isolate the host and preserve logs. Find and disable persistence before you touch credentials, because some implants watch for revocation. The payload in the TensorLake compromise polled a stolen GitHub token, and revoking that token while its monitor ran would trigger a dead-man switch that wipes the user's home directory, no matter where the revocation happened. Then rotate every credential the account could reach, from a separate clean machine, since a live RAT can watch what you type on the infected one. Then rebuild.
Uninstalling the package, deleting node_modules, and running npm ci restores your project, not your machine.
Strong authentication does not protect a maintainer whose laptop runs a RAT. Security researcher Tay, quoted in our report on the campaign against Node.js maintainers, put it this way: "When you have a RAT on your device, it grabs your post-authentication state, making 2FA irrelevant." Our report on the axios maintainer's account reached the same conclusion: nothing in the publishing pipeline, trusted publishing and 2FA included, would have stopped a release made with the maintainer's own access.
That is why we treat maintainer account takeover and RATs as one problem. Provenance and signatures prove which account or pipeline published a version. They cannot prove that the account's owner was the one at the keyboard. Until registries can tell those apart, the dependency tree is only as secure as a small number of maintainers' laptops, and the check that scales is reading what each new version does before it runs.
Manifest confusion is a mismatch between an npm package's registry manifest and the package.json in its tarball, two copies that tools trust inconsistently.
A software bill of materials (SBOM) is a machine-readable inventory of the components, versions, and dependency relationships inside a piece of software.
Software composition analysis (SCA) inventories the third-party code in an application and checks each component for vulnerabilities, licenses, and malware.