flawopen.com/Incidents/shai-hulud-npm-worm-september-2025
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]
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
Incident Timeline
GitHub says it was notified of the Shai-Hulud attack.[1]
GitHub reported removing more than 500 compromised packages and blocking uploads matching the malware indicators.[1]
Step-by-Step Attack Flow
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.
def install_plan(reviewed_lockfile, inherited_env):
# Returns a plan only: no package installation or execution.
return {"argv": ["npm", "ci"],
"env": dict(inherited_env)}
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
- ✓Require a reviewed lockfile and npm ci --ignore-scripts in the installation job; assert the command and empty credential environment in CI policy tests. Run any necessary build hooks only after review in a separate isolated job.[3]
- ✓Keep publishing and cloud credentials out of dependency-install jobs, including home directories, mounted files and runner identities. Test the job cannot obtain publishing credentials.[1], [2]
- ✓Use trusted publishing where supported and restrict the authorized workflow, branch and environment; require review for changes. Require phishing-resistant account MFA and remove unused publish tokens.[1]
- ✓On confirmed exposure, isolate and rebuild affected runners, revoke exposed credentials, and audit package versions and workflow changes against the incident reports. Review dependency source changes: a lockfile or a release cooldown alone cannot certify safety.[1], [2]