FlawOpen

flawopen.com/Incidents/shai-hulud-npm-worm-september-2025

● npm ecosystem · CWE-506 / CWE-829 · September 2025 · Critical
FlawOpen Security Research

Shai-Hulud: the September 2025 npm worm and install-time trust

The September 2025 Shai-Hulud campaign used compromised npm maintainer accounts and malicious postinstall hooks to steal credentials and publish additional infected packages.[1]

💡 Plain English Explainer (ELI5)

Imagine a factory receiving a parts box that runs its own unpacking instructions. Those instructions copy the factory’s shipping badges and use them to send similarly tampered boxes to other factories. Disabling automatic unpacking instructions closes that entry point, but the parts themselves still need inspection before use.[1]

Core Concepts & Subsystem Terms

Lifecycle hook
In the September campaign, npm postinstall ran malicious code during installation. This is package-supplied behavior, not an npm memory-corruption CVE.
Publishing credential
An npm token grants its configured account or package permissions. Stolen publishing authority let the worm spread; expiry, scope and MFA requirements depend on policy.
Trusted publishing / OIDC
OpenID Connect lets an authorized CI workflow obtain short-lived publishing credentials without a stored npm publish token. It does not make a compromised authorized workflow safe.

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-506 CWE-829

Incident Timeline

2025-09-14

GitHub says it was notified of the Shai-Hulud attack.[1]

2025-09-22

GitHub reported removing more than 500 compromised packages and blocking uploads matching the malware indicators.[1]

Step-by-Step Attack Flow

Step 1

Compromised release

A maintainer account published a modified package. Credential phishing was a proposed initial route, not an established explanation for every affected account.[1], [2]

Step 2

Install-time execution

September’s postinstall hook executed in the installing user or CI context. November’s preinstall behavior must not be retroactively attributed to this wave.[1], [3], [2]

Step 3

Credential exposure

The malware searched developer configuration and environment data for npm, GitHub and cloud credentials, exposing more than publishing tokens.[1], [2]

Step 4

Replication

Stolen publishing credentials enabled modification and publication of other packages the victim could maintain, extending the campaign.[1], [2]

Source Code: Flaw vs. Secure Implementation

Safe Python installation-plan comparison, not malware: neither function executes npm, reads credentials or contacts a registry. The example targets npm v10 semantics documented in the references. The hardened plan requires a reviewed lockfile, suppresses lifecycle scripts and passes no inherited environment. An actual runner must provision a known npm executable and isolated home/configuration without credential files or mounts. --ignore-scripts does not make later imports or explicitly invoked scripts safe; lockfile integrity does not certify benign code.

UNPATCHED FLAW
def install_plan(reviewed_lockfile, inherited_env):
    # Returns a plan only: no package installation or execution.
    return {"argv": ["npm", "ci"],
            "env": dict(inherited_env)}
HARDENED SECURE PATCH
def install_plan(reviewed_lockfile, inherited_env):
    # Returns a plan only: no package installation or execution.
    if reviewed_lockfile is not True:
        raise ValueError("Review the lockfile before installation")
    return {"argv": ["npm", "ci", "--ignore-scripts"],
            "env": {}}

Engineering & System Hardening Checklist

Sources