
Upwind Security was first to report a malicious release of keyv, a package drawing approximately 154 million weekly downloads. For teams running Node.js in production, the practical question is narrow: did this reach us, and what needs to happen if it did.
Upwind's assessment answers the first part bluntly. "Any machine that ran npm install against an affected package version has already executed attacker-controlled code with the privileges of the installing user."
Exposure is defined by resolved version, not by intent. Because keyv and its related packages arrive as transitive dependencies, the absence of a direct reference in package.json proves nothing.
The affected caching releases are keyv@6.0.0, @cacheable/node-cache@3.1.2, cacheable@2.5.1, file-entry-cache@11.1.6, @cacheable/utils@2.5.1, @cacheable/memory@2.2.1, cache-manager@7.2.10, and flat-cache@6.1.24.
The affected Qlik and nebula.js releases are @nebula.js/sn-line-chart@2.7.1, @qlik/sdk@0.28.1, @nebula.js/stardust@7.1.2, @nebula.js/cli@7.1.2, @qlik/browserslist-config@3.0.2, @nebula.js/cli-build@7.1.2, @nebula.js/cli-serve@7.1.2, and @nebula.js/cli-sense@7.1.2.
Check lockfiles for exact resolved versions. Check SBOMs. Check CI/CD pipeline history, since a runner that pulled an affected version and was then torn down still exfiltrated whatever it held.
Upwind's direct detection method is a filesystem scan. The files setup.mjs and Math_Symbol.js are not legitimate components of any caching library, so their presence in node_modules is conclusive. For the Qlik campaign, the equivalent artifacts are setup.mjs, math_init.js, and temporary paths matching /tmp/bun-dl-*.
Pin or downgrade affected packages immediately. Lock them to the last known-good version in the lockfile, then block the compromised versions at the registry or firewall level. Registry-level blocking matters because a lockfile fix on one repository does not protect the next developer who runs a fresh install elsewhere.
Remove compromised versions from all environments, including build caches and container images that may carry a resolved copy.
Upwind's position is that any machine or CI/CD runner that installed an affected version should be treated as compromised.
The confirmed collection targets are AWS credentials, GitHub tokens, npm registry credentials, and HashiCorp Vault tokens. Upwind's broader exfiltration list covers "environment variables, AWS/cloud credentials, SSH keys, and system reconnaissance data."
Rotation should cover AWS keys, SSH keys, API tokens, and environment secrets. npm publishing credentials warrant priority, since that access is what allows a single compromise to propagate into additional packages.
Both campaigns Upwind documented depend on the same enabling condition. Lifecycle scripts execute automatically during installation, which means the attacker never needs a developer to call anything.
Upwind's structural recommendation is to disable install scripts in CI/CD by adding the ignore-scripts flag to npm install invocations, or setting it as configuration. Preinstall and postinstall hooks then require explicit review before they run.
This is the control that would have prevented the initial compromise in both cases. It is also the one that survives the next campaign, because it does not depend on knowing which packages were poisoned.
Reviewing pipeline logs for execution of setup.mjs, or for outbound connections to Bun release URLs on GitHub, determines which runs actually executed the payload. Upwind's guidance for the Qlik campaign points at npm logs and system logs for references to setup.mjs, bun-v1.3.13, or math_init.
The distinction between installed and executed matters for scoping. A package present in a lockfile that was never installed on a given machine is a different problem from one that ran.
Upwind's two reports describe the same method applied to different ecosystems. The caching campaign spans multiple maintainer namespaces, which Upwind attributes to "either a coordinated multi-account compromise or a single threat actor with access to the @cacheable, keyv, and related ecosystems." The Qlik campaign sits within one organization, and Upwind assesses that the breadth "suggests the attacker had write access to the entire Qlik/nebula.js npm organization."
Different access paths, identical outcome. The lesson Upwind draws from the keyv incident applies to both: highly trusted, high-volume dependencies remain prime targets for supply chain attackers, and a single malicious release can carry ecosystem-wide consequences.