feat(security): defend against mini-shai-hulud 2nd wave#48
Conversation
The Flatt Tech write-up on mini-shai-hulud 2nd
(blog.flatt.tech/entry/mini_shai_hulud_2nd) describes an npm worm that
propagates via optionalDependencies pointing at attacker-controlled
forks and a `prepare` script runner. This adds layered defenses:
- .npmrc: ignore-scripts, minimum-release-age=7d, audit-level=high
- bunfig.toml: disable dependency lifecycle scripts, frozen lockfile
- scripts/security/check-supply-chain.mjs: audit for IOC filenames
(tanstack_runner.js, router_init.js), C2 strings (git-tanstack.com,
filev2.getsession.org, blackmail token description, gh-token-monitor),
non-registry optionalDependencies, and unexpected lifecycle scripts
- Makefile + .github/workflows/{ci,pages}.yml: run audit before install
so a poisoned tree is rejected before any dependency code executes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a Node audit script that scans manifests, tracked files, and a suspect workflow for IOC and dangerous lifecycle scripts; hardens install behavior via ChangesSupply-chain Security Hardening
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.npmrc:
- Line 15: Replace the invalid npm config key "minimum-release-age" with the
correct key "min-release-age" and set its value to the intended number of days
(e.g., "7" for a 7‑day gate) so the npm setting is recognized and enforced;
specifically update the line containing "minimum-release-age=10080" to
"min-release-age=7" (units are days).
In `@bunfig.toml`:
- Around line 18-19: The Toml key `[install.scripts] enabled = false` is invalid
and won't disable lifecycle scripts; update the bun configuration to use the
documented setting by removing the invalid `[install.scripts]` block and set
`ignoreScripts = true` under the existing `[install]` table (i.e., replace the
`[install.scripts] enabled = false` entry with `ignoreScripts = true` in the
`[install]` section) so Bun will actually ignore install scripts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 21ab4601-a20d-41ca-8dd2-4305f264e0be
📒 Files selected for processing (6)
.github/workflows/ci.yml.github/workflows/pages.yml.npmrcMakefilebunfig.tomlscripts/security/check-supply-chain.mjs
CodeRabbit caught two phantom config keys, and `make install` was not actually disabling lifecycle scripts: - .npmrc: `minimum-release-age=10080` was an invented key; the npm 11 setting is `min-release-age` and the unit is days, not seconds. Switched to `min-release-age=7` so the 7-day quarantine applies. - bunfig.toml: `[install.scripts] enabled = false` is not a valid Bun option (bun silently accepts but ignores it, so lifecycle scripts still ran). Replaced with `[install] ignoreScripts = true`. - Makefile: bun does not honour `.npmrc`'s ignore-scripts or the `npm_config_ignore_scripts` env var, so `make install` was running every dependency's lifecycle scripts despite the config. Pass `--ignore-scripts` explicitly to both `bun install` invocations, and re-bootstrap husky after `make install` since the project's own `prepare` script is now skipped along with the rest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
Makefile (1)
3-12: ⚡ Quick winFactor shared install safety flags into one variable.
--ignore-scriptsis repeated in both targets; centralizing it reduces drift when flags evolve.♻️ Proposed refactor
+.PHONY: install +BUN_INSTALL_SECURITY_FLAGS := --ignore-scripts + -.PHONY: install install: @@ - bun install --ignore-scripts + bun install $(BUN_INSTALL_SECURITY_FLAGS) bun x husky @@ .PHONY: install_ci install_ci: - bun install --ignore-scripts --frozen-lockfile + bun install $(BUN_INSTALL_SECURITY_FLAGS) --frozen-lockfile🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Makefile` around lines 3 - 12, Introduce a shared Makefile variable (e.g., INSTALL_FLAGS) containing the common safety flags (currently --ignore-scripts) and use that variable in both places where bun install is invoked: the top-level bun install and the install_ci target's bun install --frozen-lockfile, so you don't duplicate the flag; keep the subsequent bun x husky invocation as-is and ensure install_ci still appends --frozen-lockfile in addition to the shared INSTALL_FLAGS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@Makefile`:
- Around line 3-12: Introduce a shared Makefile variable (e.g., INSTALL_FLAGS)
containing the common safety flags (currently --ignore-scripts) and use that
variable in both places where bun install is invoked: the top-level bun install
and the install_ci target's bun install --frozen-lockfile, so you don't
duplicate the flag; keep the subsequent bun x husky invocation as-is and ensure
install_ci still appends --frozen-lockfile in addition to the shared
INSTALL_FLAGS.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e26f4fa8-32b0-4f19-a885-b14f59f52935
📒 Files selected for processing (3)
.npmrcMakefilebunfig.toml
🚧 Files skipped from review as they are similar to previous changes (1)
- bunfig.toml
Summary
Adds layered defenses against the mini-shai-hulud 2nd wave npm worm described in the Flatt Tech write-up (200+ packages compromised starting 2026-05-12 from TanStack Router). The worm injects itself via
optionalDependenciespointing at attacker-controlled GitHub forks and executes aprepare-script runner; this PR closes both vectors and adds an IOC scanner that gates every commit and every CI run..npmrc—ignore-scripts=true,minimum-release-age=10080(7 days),audit-level=high. Blocks lifecycle-script propagation and freshly hijacked releases for any npm-flavored tooling.bunfig.toml—install.scripts.enabled = false(Bun refuses dependency lifecycle scripts) plusfrozenLockfile = true. Neutralises the worm's primary injection step for Bun installs.scripts/security/check-supply-chain.mjs— Audits for the four IOC categories from the write-up:tanstack_runner.js,router_init.jsgit-tanstack.com,filev2.getsession.org,IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner,gh-token-monitoroptionalDependencies(github:,git+,https?:,file:) — the actual injection vectorpackage.json(rootprepare: huskyis allowlisted)gh-token-monitorbefore rotating tokens — the wormrm -rf ~/'s on revoke.make security_audittarget, runs first inmake before-commit(gates the husky pre-commit hook); added pre-installAudit supply chainstep to bothci.ymlandpages.ymlso a poisoned tree is rejected before any dependency code can execute.Test plan
make security_auditpasses clean onmain(exit 0, "no indicators found")make before-commitruns the full chain: audit → lint:text → biome → typecheck → 66 tests → vite build, all greenpreparescript)ci.ymlandpages.yml)🤖 Generated with Claude Code
Summary by CodeRabbit