loader-utils-kit: the pino-testkit npm dropper is back under a new name
The obfuscated lib/contract.js dropper first seen in pino-testkit has been republished as loader-utils-kit, so name-based blocklists miss it and teams should search by file hash as well as by name.
Automated engineering analysis by the Khalid Aboubakr Engineering Desk, from the primary sources listed below. Not reviewed by a person before publication. About the desk
Bottom line
loader-utils-kit 4.2.1 and 4.3.5 are malware, not a vulnerable library, and there is no fixed version: the only remediation is removal plus incident response. It carries the same lib/contract.js payload as the earlier pino-testkit package, so search lockfiles and node_modules for both names and the shared file hash. Treat any developer machine or CI runner that installed or imported either package as fully compromised and rotate its secrets from a clean machine.
What happened
GitHub published advisory GHSA-2hvh-9r5r-6xj3 on 2026-11-05, classifying the npm package loader-utils-kit as malware with critical severity. The advisory lists versions 4.2.1 and 4.3.5 as affected and lists no patched version. The weakness is CWE-506, Embedded Malicious Code.
According to the advisory, loader-utils-kit ships a lib/contract.js that is byte-identical to the payload in pino-testkit, an obfuscated module that decodes and evaluates code at load. The advisory calls it a new name for the existing pino-testkit campaign (MAL-2026-16484) and says the file hashes match that record's evidence. The OpenSSF malicious-packages record for the new name, MAL-2026-18689, is dated 2026-10-26 and lists one indicator: lib/contract.js with SHA-256 c6a9028513792f288fec1d7c217c55d9541d96d436cb119ef0553c3fd49d6c41. It lists no domains, IPs or URLs.
The package presents itself as an ordinary utility, and the advisory says the code runs on import or install without the user knowing.
The earlier record, published 2026-09-23, covers pino-testkit@10.4.5. That package impersonated the pino logger by copying its description, author and README. The analysis on that record says lib/contract.js rebuilds the Function constructor from obfuscated strings, places require and module on the global object, then builds and runs a second decoded function body. The dynamically constructed code can therefore load arbitrary further modules.
Why it matters
This is not a bug with a patch window. It is a package whose purpose is to run attacker-controlled code on whatever machine loads it. The pino-testkit analysis describes the shape as arbitrary code execution for the publisher on any consumer that requires or uses the package. Arbitrary code execution on a developer laptop or CI runner would put within reach whatever that host holds, such as npm tokens, cloud credentials, SSH keys and source.
What is new here is the rename. Teams that responded to pino-testkit by blocking that one name are not protected against loader-utils-kit. The new name also drops the logger impersonation for a generic utility name, which is less likely to stand out in code review or dependency diffs. The constant across both names is the file hash, which is the more durable detection signal.
The structured advisory shows a CVSS score of 0 alongside critical severity and type malware; do not let a score-based triage filter push this down the queue.
Engineering assessment
Our assessment: for anyone who actually installed or imported either package, this is a full-compromise incident, not a dependency upgrade. The registry's own guidance on the pino-testkit record says so, and the byte-identical payload makes it reasonable to apply the same posture to loader-utils-kit. The evidence does not say how widely either package was installed, so exposure has to be established by checking, not assumed.
Three points the announcement leaves implicit:
- Name-based controls failed once already. A campaign that has renamed once can rename again. Hash-based searches for
lib/contract.jsacrossnode_modules, build caches and container images are more robust than adding one more name to a denylist. Even hash matching only catches byte-identical copies; a lightly modified payload would evade it. --ignore-scriptsis not a sufficient defence here. The pino-testkit analysis shows the payload reached throughrequire()at load or call time, not only through an install hook. Disabling lifecycle scripts reduces some risk, but importing the package still runs the code.- The exposure window is longer than the GitHub date suggests. The OpenSSF record is dated 2026-10-26, ten days before the GitHub advisory. Tools that rely only on the GitHub Advisory Database may not have flagged the package before 2026-11-05. Look back at least to late October for installs.
What the evidence does not support: there is no published description of the second stage for loader-utils-kit, and no network indicators. The desk cannot say what was exfiltrated or downloaded, so incident scope has to assume the worst for any secret that was reachable from the affected host.
Is this you?
- Any
package-lock.json,npm-shrinkwrap.json,yarn.lockorpnpm-lock.yamlin your repositories referencesloader-utils-kit(4.2.1 or 4.3.5) orpino-testkit(any version, including 10.4.5). - Any
node_modules, build cache or container image contains alib/contract.jswith SHA-256c6a9028513792f288fec1d7c217c55d9541d96d436cb119ef0553c3fd49d6c41. - A developer added either package directly, for example while looking for a test helper for pino or a generic loader utility.
- CI runners, build agents or Docker builds installed dependencies from a branch that included either package, including feature branches that were never merged.
If none of these match, you are not exposed to these two package names based on current evidence.
What to do
- Inventory now, across every repository and image. Useful read-only checks:
npm ls loader-utils-kit pino-testkit --allin each project.grep -rE "loader-utils-kit|pino-testkit" --include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml .across checked-out repositories.find / -path '*/node_modules/*/lib/contract.js' -exec sha256sum {} + 2>/dev/null | grep c6a9028513792f288fec1d7c217c55d9541d96d436cb119ef0553c3fd49d6c41on developer machines, runners and unpacked images.
- If you find either package installed or loaded, follow the registry guidance published on the pino-testkit record, which the desk considers applicable because the payload is identical: treat the machine as fully compromised; rotate all secrets and keys stored on it, doing the rotation from a different machine; remove the package, knowing removal alone does not guarantee other malicious software is gone. Prefer rebuilding the host or runner from a known-good image.
- Rotate CI secrets (npm publish tokens, cloud keys, deploy keys, signing keys) for any pipeline that ran an install including either package, and review recent publishes and deployments made with those credentials.
- Block both names and the hash in your registry proxy, dependency policy or software composition analysis tool. Not vendor guidance.
- Make sure your scanning uses the OpenSSF/OSV malicious-packages feed as well as the GitHub Advisory Database, given the ten-day gap between the two records for this package. Not vendor guidance.
- For technical leads: require review for any new dependency that arrives in a lockfile diff, commit lockfiles and install with
npm ci, and keep long-lived credentials off developer machines and build agents where possible. Not vendor guidance.
There is no version to upgrade to. Do not wait for one.
What we don't know yet
- What the second-stage code in
loader-utils-kitdoes. The record lists no domains, IPs or URLs, so exfiltration or download behaviour is not stated. - Whether other package names carry the same
lib/contract.jshash. - Whether npm has removed
loader-utils-kit4.2.1 and 4.3.5, and how many installs occurred, including between 2026-10-26 and 2026-11-05. - Whether the compromise and rotation guidance on the pino-testkit record is intended to apply unchanged; the
loader-utils-kitadvisory itself gives no remediation text. - Why the finder credit differs between records (OpenSSF in the GitHub advisory, CyberXYZ in the OpenSSF JSON file).
The desk will watch the OSV and GitHub records for additional names, indicators or second-stage analysis and update this story in Engineering Desk news.
Sources
Latest engineering analysis
6 min read
solidity-map on npm: same malware payload, third package name
GitHub's advisory database flags solidity-map 2.21.0 as malware that reuses the hardhat-roof and chai-foundry payload byte for byte, so name-based blocklists will keep missing the next rename.
Engineering Practice6 min read
Laravel 13.40.0: transaction, replica and Redis changes to test first
A point release that changes how database-heavy Laravel apps behave under failure, plus a security-flavoured Concurrency fix that ships with no advisory.
6 min read
SD-JWT JS silently drops tampered disclosures and reports success (CVE-2026-62374)
A verifier-side conformance gap in the OpenWallet Foundation's SD-JWT JavaScript libraries lets a credential holder silently remove a claim, and key binding does not stop it.