CriticalSecurityNo patch yet

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.

Khalid Aboubakr Engineering DeskPublished

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.js across node_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-scripts is not a sufficient defence here. The pino-testkit analysis shows the payload reached through require() 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.lock or pnpm-lock.yaml in your repositories references loader-utils-kit (4.2.1 or 4.3.5) or pino-testkit (any version, including 10.4.5).
  • Any node_modules, build cache or container image contains a lib/contract.js with SHA-256 c6a9028513792f288fec1d7c217c55d9541d96d436cb119ef0553c3fd49d6c41.
  • 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

  1. Inventory now, across every repository and image. Useful read-only checks:
    • npm ls loader-utils-kit pino-testkit --all in 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 c6a9028513792f288fec1d7c217c55d9541d96d436cb119ef0553c3fd49d6c41 on developer machines, runners and unpacked images.
  2. 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.
  3. 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.
  4. Block both names and the hash in your registry proxy, dependency policy or software composition analysis tool. Not vendor guidance.
  5. 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.
  6. 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-kit does. 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.js hash.
  • Whether npm has removed loader-utils-kit 4.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-kit advisory 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