---
title: "forge-jsx npm Package: Purpose-Built Multi-Platform RAT"
description: "forge-jsx poses as an Autodesk Forge SDK on npm. On install it deploys a system-wide keylogger, recursive .env file scanner, shell history exfiltrator, and a WebSocket-based remote filesystem backdoor to C2 at 204.10.194.247, with persistence via systemd, LaunchAgent, and Task Scheduler."
url: "https://safedep.io/blog/malicious-forge-jsx-npm-rat"
author: "SafeDep Team"
published: "2026-04-15"
---

# forge-jsx npm Package: Purpose-Built Multi-Platform RAT

## 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: `john@taohunter.ai`), 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`:

```json
"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:

```javascript
// scripts/postinstall-bootstrap.mjs
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:

```javascript
// scripts/postinstall-bootstrap.mjs
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:

```javascript
// dist/deploymentCipherData.js
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:

```json
{ "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`:

```javascript
// dist/windowsInputSync.js
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:

```javascript
// dist/envScan.js
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 directories
- `fs_list`: directory contents
- `fs_read`: arbitrary file content (chunked for large files)
- `fs_zip`: zip and stream a directory tree
- `fs_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            | Email                        | Package                           |
| ------------------ | ---------------------------- | --------------------------------- |
| `johnceballos0716` | `johnceballos0716@gmail.com` | `forge-jsx` v1.0.0–v1.0.6         |
| `johntaohunter`    | `john@taohunter.ai`          | `@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.
