Import-Time npm Malware Bypasses --ignore-scripts

On 14 July 2026, five packages in the AsyncAPI ecosystem were republished with malicious code inside them. Three days earlier, the jscrambler package went through something similar. Neither campaign needed a postinstall hook. Both fired the moment the code was imported.

That detail is the whole story — and it invalidates a mitigation that a lot of pipelines currently treat as sufficient.

The assumption that just broke

For years the npm threat model had a comfortable shape: a malicious package runs its payload during installation, through preinstall or postinstall lifecycle hooks. Defences followed that shape. Scanners flagged lifecycle hooks. CI pipelines added --ignore-scripts. And npm 12 made install scripts opt-in by default, which was a real improvement.

Unit 42's tracking of npm campaigns through the first half of 2026 reads like a catalogue of that single pattern: Bitwarden (22 April), SAP/CAP (29 April), TanStack (11 May), @antv (19 May), Red Hat (1 June) — preinstall hook after preinstall hook.

Then the door closed, and the payload moved somewhere the door does not cover.

What changed

In the AsyncAPI compromise, the affected releases — @asyncapi/specs 6.11.2 and 6.11.2-alpha.1, @asyncapi/generator 3.3.1, @asyncapi/generator-components 0.7.1, @asyncapi/generator-helpers 1.1.1, all republished inside roughly 90 minutes — declared no lifecycle hooks at all. Microsoft's write-up is blunt about the consequence: "When any consuming build or application imports a poisoned package, the injected block runs immediately," and "the common npm install –ignore-scripts mitigation does not neutralize it."

The jscrambler incident shows the same evolution compressed into a single day. Versions 8.14.0 through 8.17.0 used a preinstall hook pointing at dist/setup.js. From 8.18.0 onward the dropper was injected straight into dist/index.js and the CLI entry point — self-executing on import, surviving --ignore-scripts, and invisible to any scanner that only reads package.json hooks.

What an import-time dropper looks like

Nothing exotic. A module body executes on require/import, so a top-level side effect is the entire technique:

js
// dist/index.js — real exports below, dropper above
const { spawn } = require('node:child_process')

spawn('node', [payloadPath], {
  detached: true,      // survives the parent
  stdio: 'ignore',     // nothing in your build output
  windowsHide: true,   // no console window
}).unref()             // parent exits normally

module.exports = require('./real-entry.js')

The parent process finishes cleanly and the build log stays green. In the AsyncAPI case, stage one then pulled an ~8.2 MB encrypted bundle from IPFS by hardcoded CID, decrypted it with embedded HKDF-SHA256 / AES-256-GCM keys, and started a modular command-and-control runtime with its own persistence.

Since --ignore-scripts no longer decides the outcome, the leverage moves upstream — to which versions are ever allowed onto the machine:

jsonc
{
  "overrides": { "@asyncapi/specs": "6.11.1" },
  "scripts": {
    "ci": "npm ci --ignore-scripts && npm audit signatures"
  }
}

Caveats worth respecting

Lockfiles are necessary, not sufficient. They pin a version, but a compromised version that lands in the lockfile before detection is pinned just as faithfully. Pair pinning with a publish-age delay so brand-new releases are never the ones your build reaches for.

Purge the caches. Microsoft's guidance explicitly includes clearing npm and Yarn caches — a poisoned tarball sitting in a cache outlives the registry takedown.

Rotate credentials from build machines, not just laptops. Both payloads harvested cloud credentials, tokens, and AI tool configuration files containing API keys. CI runners hold the most valuable set.

Watch egress, not just packages. Blocking IPFS gateways at the perimeter breaks stage two even when stage one already ran.

Takeaway

--ignore-scripts is still worth keeping — it closes a real door. It just stopped being a boundary. Anything a dependency can do at import time, it can now do without asking, which means the meaningful controls are version pinning, publish-age delays, cache hygiene, and egress restriction on build infrastructure.

Primary sources: Microsoft Security Blog — AsyncAPI npm compromise and Socket — jscrambler supply chain attack.