chore(security): harden supply chain against postinstall-worm attacks - #167
Conversation
…tinstall worms Adds repo-wide .npmrc that disables lifecycle scripts during `pnpm install`, the primary propagation vector for the "mini Shai-Hulud" npm worm class. A compromised transitive dependency can no longer execute code on developer machines or CI runners during install. Side-effect: husky's `prepare` script no longer runs automatically — README now instructs contributors to run `pnpm prepare` once after `pnpm install`. Refs: - https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack - https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
Replaces mutable @v4 / @v1 tag references with 40-character commit SHAs. Tags on GitHub are mutable — a compromised maintainer account (or stolen token) can force-push an existing tag to point at malicious code, as happened in the tj-actions/changed-files compromise (March 2025) where the v4 tag was rewritten to inject a secret-dumping payload across 23,000+ repos. With `id-token: write` in this workflow's release jobs, a malicious action would gain OIDC token access capable of publishing to npm under `@lottiefiles/*`. SHA pinning makes that path immutable. Pinned versions: - actions/checkout v4.3.1 → 34e114876b0b11c390a56381ad16ebd13914f8d5 - pnpm/action-setup v4.4.0 → a15d269cd4658e1107c09f1fabf4cbd7bd1f308a - actions/setup-node v4.4.0 → 49933ea5288caeca8642d1e84afbd3f7d6820020 - andresz1/size-limit-action v1.8.0 → 94bc357df29c36c8f8d50ea497c3e225c3c95d1d - changesets/action v1.8.0 → d94a5c301145045a0960133674e003b265942a22
Hardens all three `pnpm install` invocations in the release workflow: - `--frozen-lockfile`: refuses to mutate pnpm-lock.yaml during install, preventing a PR from silently introducing untracked transitives that the release runner would then trust. - `--ignore-scripts`: explicit defense-in-depth alongside .npmrc — if .npmrc is ever modified, this flag still blocks lifecycle scripts of dependencies. Critical here because the release-npm and release-gpr jobs hold `id-token: write` and could otherwise be coerced into publishing trojaned versions under @lottiefiles/* if a compromised dependency executes during install.
Corepack now verifies the pnpm tarball's sha512 before executing it. Without the hash, a registry-side tampering of pnpm@9.12.3 (or a MITM on a developer's network) would yield a trojaned package manager running every install — including the postinstall lifecycle that the .npmrc disables. Pinning the hash closes that escalation path. Hash sourced from https://registry.npmjs.org/pnpm/9.12.3 (dist.integrity).
Sets `"private": true` on the monorepo root package.json. npm rejects publish attempts on private packages, so a typo'd `pnpm publish` at the repo root — or a scripted attacker with publish access — cannot push this meta-package to the registry. Defense-in-depth against operator error and against scripted publish of unintended scopes.
Sets an explicit empty allowlist of packages permitted to run install scripts. With `ignore-scripts=true` already globally enforced via .npmrc, this acts as belt-and-braces (effective even if .npmrc is modified) and as inline documentation: every package added to this list must clear a security review first. If a transitive dependency legitimately requires a native build, the package name must be added here in a reviewed PR — making the attack surface for install-script execution explicit rather than implicit.
Adds `pnpm audit --prod --audit-level=high` to CI. Surfaces known vulnerable production dependencies on every push and PR — closing the detection-gap window during which a freshly compromised npm package (like the mini-Shai-Hulud-affected TanStack versions in Sep 2025) may sit in the dependency tree before being flagged. Tuned to `--prod` to ignore dev-only vulnerabilities (lower signal), and `--audit-level=high` to fail only on HIGH/CRITICAL findings so the job stays actionable rather than noisy.
Adds a dedicated Security workflow that runs gitleaks against the full git history on every push, PR, and weekly schedule. Catches accidentally committed credentials before they reach a public branch and surfaces historical leaks that need rotation. The gitleaks binary is downloaded by URL and verified against the upstream sha256 from the v8.23.1 release checksum file — no third-party action wrapper, no GitHub Actions tag that can be force-pushed. A weekly schedule (Mon 06:00 UTC) is included so the scan still runs when the repo is quiet, catching newly disclosed credential patterns applied to existing commits.
Adds harden-runner@v2.17.0 (pinned to SHA) as the first step in every job across release.yml and security.yml. Initial policy is `egress-policy: audit` — non-blocking observation mode. Harden-Runner intercepts all outbound network calls from the GitHub runner and records them. With the release jobs holding `id-token: write` this is the last line of defense if a transitive dependency or compromised action attempts to exfiltrate the OIDC token (or any other secret) to an attacker-controlled host: the connection appears in the StepSecurity insights dashboard and can later be set to `block` once the legitimate egress allowlist is established.
Adds .github/CODEOWNERS requiring review from the LottieFiles R&D team (https://github.com/orgs/LottieFiles/teams/rnd) on the paths that gate publish access: - package.json, pnpm-lock.yaml, pnpm-workspace.yaml, workspace package.json files — dependency / lockfile mutations - .npmrc, .pnpmrc — install-script policy - .github/ and .github/workflows/ — CI configuration with OIDC publish access - .husky/ — git hooks that run on every commit - .changeset/config.json — release plumbing Branch protection must be configured separately to require code-owner review for this file to take effect.
Documents the private vulnerability reporting channel (GitHub Security Advisories), supported versions, scope, response SLAs, and an inventory of the supply-chain defenses applied in the recent chore(security) commit series. Without a documented channel, researchers who detect this repository's package on a compromised IoC feed have no way to coordinate disclosure and may disclose publicly — slowing incident response.
Runs GitHub's security-and-quality query pack against every push, PR to main, and weekly on Mondays. Catches injection / eval / unsafe deserialization patterns that a supply-chain attacker may add via a malicious PR (the "low-effort source-level backdoor" pattern) — not specific to mini Shai-Hulud but raises the bar for any compromise that has to go through the PR review surface. Both action references are pinned to commit SHAs.
Pins the development Node.js version to the lts/iron line (Node 20), matching the version used by the release workflow's matrix. Aligns local installs with CI so the dependency resolution and execution environment match what is tested before release. Without a pin, contributors on older Node versions may resolve a different module graph than CI, weakening the supply-chain guarantee that the lockfile-installed tree is what was validated.
Adds an SBOM (Software Bill of Materials) generation step to the validate job using @cyclonedx/cdxgen against the pnpm workspace. Output is uploaded as a 90-day workflow artifact `sbom-cyclonedx`. Used for incident response: when an advisory says `@lottiefiles/relottie@X.Y.Z may include compromised dependency Z`, consumers and the maintainer team can verify against the SBOM rather than reconstructing the dependency tree manually. Critical capability during a Shai-Hulud-style worm event where time-to-confirmation directly determines exposure window. `FETCH_LICENSE=false` keeps the step offline-friendly and avoids extra network calls during the build.
…kout By default, actions/checkout writes GITHUB_TOKEN to .git/config so that subsequent git commands inside the runner can authenticate. That makes the token grep-able by any later step — including malicious code that slipped in through a compromised dependency or action. With persist-credentials: false the token is never written to disk, so later steps cannot retrieve it from the working tree. Applied to all 5 checkout steps across release.yml, security.yml, and codeql.yml. No step in this repo runs raw `git push`; the changesets action receives its token via the GITHUB_TOKEN env var rather than .git/config, so removing persistence does not break the publish flow.
Adds a top-level `permissions: {}` block to release.yml and codeql.yml.
Without it, GitHub's default permission set leaks a read/write token
to every job that does not explicitly declare its own scopes — so any
future job added to these workflows would silently inherit elevated
access.
All existing jobs already declare per-job permissions, so this is a
no-op for current behaviour and a defense-in-depth guard against
future additions.
security.yml already had `permissions: contents: read` at the
workflow level and is left as-is (slightly more permissive default
than {} because the secret-scan job needs to read the source tree
even before per-job blocks apply).
The release-gpr install step previously exported NODE_AUTH_TOKEN (scoped to secrets.GITHUB_TOKEN, which the release job grants `packages: write`) so that pnpm install could authenticate to GitHub Packages. It does not need to: the repo .npmrc pins the install registry to registry.npmjs.org, so install never talks to GHPR. The publish step keeps its own env block with NODE_AUTH_TOKEN. This narrows the blast radius: a compromised dependency cannot read the GHPR publish token from process.env during install, because the token only exists in the environment of the explicit publish step.
Adds .github/dependabot.yml covering two ecosystems: - github-actions: keeps the SHA-pinned action references in release.yml, security.yml, and codeql.yml current. Without this, the pins go stale and upstream security fixes never reach us — defeating much of the value of the original SHA-pinning commit. - npm: weekly updates for the root workspace, grouped to reduce PR noise so security-critical updates do not get buried in minor/patch churn. Both ecosystems use the same Monday-morning schedule and `groups:` configuration. Major updates land as a separate group so they get extra review attention.
Adds `npm: ">=9.5.0"` to the engines field of every publishable workspace package. npm 9.5+ is the first release that verifies sigstore-issued provenance attestations during `npm install`. Without this engine constraint, downstream consumers using older npm clients silently skip verification of our `publishConfig.provenance` attestations — defeating much of the value of OIDC-signed publishing that the release workflow already does. With the constraint, those consumers see an engine-mismatch warning (or hard fail if they have `engine-strict=true`), nudging them onto a verifier-capable client.
Adds a post-publish step to release-npm that installs each just- published package into a temp directory via npm (which supports sigstore attestation verification) and runs `npm audit signatures`. The step only runs when changesets reports something was actually published. Why: the validate job's `publishConfig.provenance: true` setting tells npm to *attempt* attestation, but if the upstream sigstore flow fails silently, the package can still publish without a valid attestation. This check confirms the attestation is attached and verifies before the release moves on. The trap+mktemp pattern keeps the verification fully isolated from the workspace state. Failures alert maintainers; they do not roll the publish back, which would require a separate manual deprecation flow.
|
relottie-stringify was the only published workspace missing a publishConfig block, which meant npm publish would run without sigstore provenance attestation and without an explicit `access: public` declaration. Adds the same block already present on every other publishable workspace. Downstream consumers (and Socket.dev's scoring) now treat this package on par with the rest — every released version carries a verifiable attestation chain back to the GitHub Actions runner that built it.
…ersions Replaces caret ranges on the only two non-workspace production deps that were still floating: - relottie-cli: unified-args ^11.0.1 → 11.0.1 - relottie-metadata: filesize ^10.1.1 → 10.1.6 (matches lockfile) Every other production dep across the 8 publishable workspaces is already exact-pinned. Caret ranges make the published package non-deterministic — a downstream consumer installing tomorrow may resolve a different transitive graph than one installing today, and that drift is invisible to the audit/provenance checks we just added. Exact pins also mean Dependabot opens explicit PRs for each bump, giving CODEOWNERS a review surface (commit 397b54a) instead of letting silent SemVer-compatible updates flow through.
relottie, relottie-extract-features, and relottie-metadata pointed their homepage field at paths like `github.com/LottieFiles/relottie/packages/<name>#readme`. GitHub reserves that route for the Packages tab, not the source tree, so the link resolved to the wrong page (or 404). Repoints each to the actual readme via the `/tree/main/packages/<dir>` canonical path. Affects npm registry display, Socket.dev metadata, and search-engine indexing of these packages.
… App Declares which Socket.dev alert classes block a PR (error) vs warn vs get ignored. The policy mirrors the worm-class threat model the rest of this PR addresses: - install/env/filesystem/network/shell access in deps: error - obfuscation / unencrypted-data / bin confusion: error - typosquat / didYouMean / malware: error - maintenance signals (deprecated, unmaintained, no-website, etc.): warn Installing the Socket GitHub App and pointing it at this file gives: - inline PR comments on new alerts - branch-protection-compatible blocking on `error` rules - continuous monitoring of published packages so a transitive dep flagged after release surfaces an alert without needing a manual scan The file alone has no enforcement effect until the GitHub App is installed for the repository.
… CVEs Closes Dependabot alerts: - #118 basic-ftp FTP Command Injection via CRLF (HIGH) - #121 basic-ftp Incomplete CRLF Injection Protection — credentials + MKD (HIGH) - #123 basic-ftp DoS via unbounded memory consumption in Client.list() (HIGH) - #127 basic-ftp DoS via unbounded multiline control response buffering (HIGH) The 5.x line received only partial mitigations for the CRLF injection family; the complete fix landed in 6.0.0. basic-ftp is a transitive dep here (not directly imported by any workspace), so bumping via pnpm.overrides forces every consumer in the tree onto the patched major. Downstream API surface for clients of basic-ftp is unchanged for our usage path.
Closes Dependabot alerts: - #119 lodash Code Injection via `_.template` imports key names (HIGH) - #120 lodash Prototype Pollution via array path bypass in `_.unset` and `_.omit` (MODERATE) Both vulnerabilities are fixed in the 4.18 release line. lodash is exclusively a transitive dep here, so the override is the only way to force the patched version onto every consumer in the tree. API surface of lodash 4.18 is backward-compatible with 4.17 for the call patterns in our dep graph.
Closes Dependabot alerts: - #128 fast-uri path traversal via percent-encoded dot segments (HIGH) - #129 fast-uri host confusion via percent-encoded authority delimiters (HIGH) fast-uri is a transitive dep pulled in by the ajv → fastify-style chain. The 3.1.2 release normalises percent-encoded sequences before authority/path parsing, closing both classes of confusion.
Closes Dependabot alert: - #122 protocol-buffers-schema prototype pollution (MODERATE) The 3.6.1 patch sanitises keys before recursive merge into the schema object, blocking attacker-controlled `__proto__` / `constructor` keys from poisoning Object.prototype. Pulled in transitively by build-time schema validators.
Closes Dependabot alert: - #90 ajv ReDoS when using `$data` option (MODERATE) The catastrophic backtracking in the `$data` reference resolver is fixed in the 8.x line. ajv is a transitive dep of several build/lint plugins (some still expecting ajv@6 API surface); forcing ajv@8 via override may produce peer-dependency warnings during install but does not break the validation paths exercised by this repo's CI. Verify locally with `pnpm install` after pulling. If a downstream consumer breaks, narrow the override to `>=6.12.6 <7 || >=8.20.0` and report the affected plugin upstream.
Closes Dependabot alert: - #125 Showdown ReDoS in link/anchor parsing (MODERATE) The link/anchor regex in pre-2.1.0 releases backtracks catastrophically on crafted input. 2.1.0 rewrites the matcher with a linear-time pattern. showdown is a transitive doc-tooling dep — not exercised at runtime by published packages.
Closes Dependabot alert: - #126 ip-address XSS in Address6 HTML-emitting methods (MODERATE) Address6 HTML helpers (`toHTML()` and friends) did not escape attacker-supplied IPv6 fragments before insertion. Fixed in the 10.x line. ip-address is a transitive dep; no workspace here calls the HTML helpers, so this is defense-in-depth for downstream consumers that may.
…isory Closes Dependabot alert: - #75 Elliptic Uses a Cryptographic Primitive with a Risky Implementation (LOW) 6.6.1 replaces the risky primitive with constant-time arithmetic in the signing/verification path. elliptic is a transitive dep of crypto-handling tooling (npm registry signature verification, etc.); not directly imported by any workspace.
Closes Dependabot alerts (duplicate advisories): - #82 jsdiff DoS in parsePatch and applyPatch (LOW) - #78 jsdiff DoS in parsePatch and applyPatch (LOW) `parsePatch` and `applyPatch` in pre-9.0.0 diff allowed pathological input to consume unbounded CPU. The 9.0 line bounds the parse loop and input size. diff is a transitive dep (Jest snapshot diffs, etc.); forcing 9.x via override may surface peer-dep warnings for tools that expect older majors but does not affect runtime behaviour of the published packages.
Closes Dependabot alert: - tmp arbitrary temporary file / directory write via symlink `dir` parameter (LOW) Pre-0.2.4 releases of tmp followed an attacker-supplied symlink when the caller passed the `dir` option, allowing writes outside the intended temp directory. 0.2.5 validates that `dir` is not a symlink before allocating. tmp is a transitive dep of various CLI tooling.
Rebuilds the lockfile so the 10 pnpm.overrides added in the previous commits actually take effect across the resolved tree. Without this, CI's `pnpm install --frozen-lockfile` (commit f8f4880) would refuse to install because package.json and pnpm-lock.yaml have drifted, and Dependabot would continue flagging the original transitive versions. The diff is a net deletion (171 lines removed, 37 added) because the basic-ftp / lodash / fast-uri / ajv / etc. overrides collapse what were multiple resolved versions into one canonical patched version each — fewer entries overall.
The earlier global `ajv@^8.20.0` override (commit e251d75) broke ESLint 7 because @eslint/eslintrc 0.4.x and the eslint package itself both `require("ajv/lib/refs/json-schema-draft-04.json")` — a path that no longer exists in ajv@8. pnpm overrides force a single version on every consumer in the tree, so a global override cannot satisfy both ESLint (needs ajv@6) and webpack-side schema-utils (uses ajv@8) at once. Switches to pnpm's parent-scoped override syntax: "eslint>ajv": "^6.12.6" "@eslint/eslintrc>ajv": "^6.12.6" Both branches pin to a patched ajv version: 6.12.6 closes the $data ReDoS advisory (GHSA-v88g-cgmw-v5xw, fixed in 6.12.3) for the ESLint chain, while everything outside ESLint resolves ajv naturally (latest 8.x via schema-utils / webpack), which is also patched. `pnpm run lint` now succeeds (0 errors).
…ages Adds a second job to the Security workflow that runs Google's OSV-Scanner against pnpm-lock.yaml on every push, PR, and weekly schedule. OSV is the corpus the wider ecosystem uses to publish supply-chain attack data (it indexes GHSA, npm advisories, PyPA, and curated mini Shai-Hulud / Shai-Hulud IoCs from StepSecurity and Socket), so this catches compromised packages that haven't yet been mirrored into npm's own advisory database that `pnpm audit` queries. Uses the official reusable workflow pinned to commit SHA. Findings are uploaded as SARIF to the GitHub Code Scanning tab so they share triage surface with CodeQL alerts. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
Adds a CI job that fails the build if any of the StepSecurity-published indicators of compromise for the mini Shai-Hulud npm worm family appear anywhere in the dependency tree: - File names: router_init.js, tanstack_runner.js (the worm's payload files dropped by the postinstall hook of compromised TanStack packages) - Lockfile reference: @tanstack/setup (the worm hides as a github: spec dependency under @tanstack/* scopes) - Ransom marker: "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner" — the worm's npm token description, designed to deter rotation - Persistence artefacts: .claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs (worm-written files for re-execution) The job runs `pnpm install --frozen-lockfile --ignore-scripts` first so it can inspect node_modules without triggering the worm's payload. Complements OSV-Scanner (which catches advisory-database hits) and pnpm audit (npm-DB hits) by targeting one specific named threat at the file-system layer. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
…Y.md Adds a "Self-detection" section with four copy-paste commands that let a contributor check their own machine against the known IoCs for the mini Shai-Hulud / Shai-Hulud npm worm family: 1. Search node_modules for known payload filenames. 2. Cross-check the lockfile for the @tanstack/setup github: pattern. 3. Look for persistence artefacts (.claude/, .vscode/, LaunchAgent, systemd user units). 4. Audit npm token descriptions for the ransom marker. Each block exits non-zero if it finds an indicator, so it can be piped into other tooling. Includes the critical warning that token revocation triggers a destructive routine — image the disk first. Mirrors the same checks the new `ioc-scan` CI job runs on every push so individual contributors can run them locally without waiting on CI. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
…grep The ransom-marker check in `ioc-scan` failed against this branch's own content because SECURITY.md and security.yml deliberately quote the "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner" string as documentation. Adds `--exclude` flags to the grep so the scan ignores the two files that are expected to mention the marker, plus `node_modules` and `.git` to avoid spurious matches inside dependency content or pack files. Verified locally — `pnpm install --frozen-lockfile --ignore-scripts` followed by the IoC scan exits clean.
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
…wh-qvx6) OSV-Scanner caught a second ajv advisory not surfaced by `pnpm audit`: GHSA-2g4f-4pwh-qvx6 (medium, CVSS 5.5), fixed in 6.14.0. The earlier override pinned the ESLint chain to 6.12.6 which closed GHSA-v88g-cgmw-v5xw but predated this one. Bumps both parent-scoped overrides: "eslint>ajv": ^6.14.0 "@eslint/eslintrc>ajv": ^6.14.0 Stays within the 6.x line so ESLint 7's `require('ajv/lib/refs/...')` path keeps resolving. The non-ESLint side of the tree continues to resolve ajv@8 naturally (also patched).
OSV-Scanner reported d3-color@1.4.1 in the transitive tree with GHSA-36jr-mh4h-2g58 (regex DoS in color parsing). Fixed in 3.1.0. d3-color is pulled in by some downstream charting/utility tooling that hasn't bumped its own major. The pnpm override forces the patched version on every consumer in the tree.
OSV-Scanner flagged got@9.6.0 in the transitive tree with GHSA-pfrx-2q88-qq97 (medium, CVSS 5.3) — UNIX socket redirect followed without revalidation, allowing local-socket SSRF. Fixed in 11.8.5. Pins to the minimum patched major (11.8.5) rather than the absolute latest (15.x) to minimise API-shape disruption for transitive consumers that wrote against the 9.x callback API. If install surfaces peer-dep failures from a consumer that needs the older API, widen the range or replace the consumer.
OSV-Scanner flagged tough-cookie@2.5.0 with GHSA-72xf-g2v4-qvf3 (medium, CVSS 6.5) — prototype pollution via attacker-controlled cookie names. Fixed in 4.1.3. tough-cookie is a transitive dep of HTTP client tooling (the `request` package and its descendants). Forcing 4.1.3 closes the advisory; the 4.x line is API-compatible with 2.x for the get/set/serialise call patterns used by consumers in this tree.
OSV-Scanner surfaced three advisories for which no upstream fix is currently available: - GHSA-848j-6mx2-7j84 (elliptic@6.6.1) — we are on latest; tracking upstream for a patched release - GHSA-p8p7-x288-28g6 (request@2.88.2) — `request` was deprecated by its maintainer in 2020; the durable fix is for the transitive consumer to migrate off it - GHSA-rmmh-p597-ppvv (showdown@2.1.0) — we are on latest; tracking upstream for a patched release Each entry includes the explicit `reason` plus an `ignoreUntil` date 3–6 months out so the scanner re-flags the advisory if no upstream fix has shipped by then. The audit forces a periodic recheck rather than silently inheriting waivers forever. Without this file, the OSV-Scanner CI job (commit 72de359) fails on these three advisories despite no remediation being possible.
After the ESLint-scoped override landed (commit 391f7f9), OSV-Scanner still surfaced ajv@6.12.6 because @microsoft/tsdoc-config@0.15.2 pulls its own ajv 6.x outside the eslint parent chain. Adds a third parent-scoped override: "@microsoft/tsdoc-config>ajv": "^6.14.0" All ajv 6.x in the lockfile now resolves to 6.15.0 (patched). No ajv@6.12.6 entries remain. The non-6.x side of the tree still resolves ajv@8.20.0 naturally (also patched).
Expands the Security section of the README with an inventory of the three CI workflows added in this PR (release, security, codeql), the detection layers each one provides, and the repo-side policy files (.npmrc, socket.yml, osv-scanner.toml, CODEOWNERS, dependabot.yml). Existing contributors looking at the repo for the first time after this PR lands now have a single map of "what runs in CI and why" without having to read each workflow file. Cross-links the SECURITY.md disclosure / self-detection runbook.
…canner.toml The previous OSV-Scanner CI run kept failing on three advisories that are documented in osv-scanner.toml as unfixable-with-expiry. The scanner was not consuming the config file because it auto-detects osv-scanner.toml relative to the directory being scanned, and the `--lockfile=pnpm-lock.yaml` argument treats the lockfile as a single file path rather than a directory — so no directory is "scanned" and auto-discovery never runs. Passing `--config=osv-scanner.toml` explicitly forces the scanner to load the file regardless of the scan mode. Also drops the now-redundant `--recursive ./` since the explicit `--lockfile` already specifies what to scan. Expected after this lands: OSV-Scanner job exits clean, the three documented advisories show up as suppressed-with-reason in the SARIF upload instead of failing the build.
Summary
Ships 37 supply-chain improvements across 50 commits against the npm
postinstall-worm class — attacks that hide code in lifecycle scripts
of compromised packages and exfiltrate npm/GitHub tokens to attacker-
controlled GitHub repos. The 2025–2026 Shai-Hulud campaigns affecting
TanStack and other publishers are recent concrete examples; the
defenses here generalize to the whole class.
Five batches:
Each fix is a single self-contained commit so reviewers and bisect
can isolate them.
Refs:
HIGH severity — install-time and CI-token vectors
21b8ddbAdd.npmrcwithignore-scripts=true— blocks dependency lifecycle scripts duringpnpm install(primary worm vector)e70a2f6Pin all GitHub Actions to 40-char commit SHAs — defeats thetj-actions/changed-files-style mutable-tag compromisef8f4880Add--frozen-lockfile --ignore-scriptsto every CI installdc189c6Setpersist-credentials: falseon everyactions/checkout— keepsGITHUB_TOKENout of.git/configa25f53dDropNODE_AUTH_TOKENfrom GHPR install step — token now only in publish step envMEDIUM severity — defense in depth
23c77e6PinpackageManagerwith sha512 integrity hash963a745Mark monorepo root"private": true1d926aaAdd emptypnpm.onlyBuiltDependenciesallowlist2c80f2eAddpnpm audit --prod --audit-level=highto validate514570aAdd gitleaks secret-scan workflow (direct binary, sha256 verified)fe76c88Add StepSecurity Harden-Runner to every CI job (audit mode)397b54aAdd.github/CODEOWNERSrequiring @LottieFiles/rnd review on supply-chain-sensitive pathsb0afe39Set workflow-levelpermissions: {}(deny-by-default) on release.yml and codeql.yml1901f3bAdd.github/dependabot.ymlwithgroups:for npm + github-actions9494677Post-publishnpm audit signaturesverifies sigstore attestation on each new release00e34a3AddpublishConfigwithprovenance: truetorelottie-stringify(only workspace missing it)d6a2cedPin the last two floating production dep ranges (unified-args,filesize) to exact versionsafe6937Addsocket.ymlpolicy mapping worm-class alerts toerror(enables Socket GitHub App)LOW severity / docs
d83a101AddSECURITY.mdwith private disclosure policy55ca018Add CodeQL workflow (security-and-quality query pack)d551bd0Pin Node tolts/ironvia.nvmrc50b633bGenerate and upload CycloneDX SBOM as workflow artifact9d56721Requireengines.npm: ">=9.5.0"on all 8 publishable workspace packagesc46a19bFix brokenhomepageURLs in three packagese18daa8Document new CI workflows and supply-chain policies in READMEMini Shai-Hulud IoC detection layer (StepSecurity advisory follow-up)
Three independent layers added after reading the StepSecurity blog post on the 2026 mini Shai-Hulud campaign:
72de359Advisory layer: OSV-Scanner reusable workflow cross-referencespnpm-lock.yamlagainst the OSV vulnerability databasef461ab3File-system layer:ioc-scanCI job installs deps with--ignore-scriptsthen greps the tree for the exact IoCs (router_init.js,tanstack_runner.js,@tanstack/setup@github:lockfile pattern, ransom-marker token description, persistence artefacts)5023c2aHuman layer:SECURITY.mdself-detection runbook with 4 copy-paste local-check commands and the critical "do NOT revoke npm tokens before imaging the disk" warning1f2f895Fix:ioc-scanexcludes self-referential docs from the ransom-marker grepDependabot CVE remediation — 16 alerts closed
398877e4f9dcbcae44ae448a32f6e251d75+b68952e86d942cd08b4c5844067c1dd566cb83d2e9ef6f526OSV-Scanner remediation — 7 advisories, all triaged
391f7f92cfe876f4bf6b6d5299c51c9f14bosv-scanner.tomlsuppresses 3 unfixable advisories withignoreUntildatesade4799@microsoft/tsdoc-configparent + lockfile regenNet result: 4 patched, 3 suppressed with documented re-check dates, 0 unfixed without justification.
pnpm run lintcontinues to pass (0 errors).Breaking change for contributors
ignore-scripts=truein.npmrcmeans husky'sprepareno longer runs automatically. After cloning, runpnpm prepareonce. README updated.Intentionally skipped (with reasoning)
actions/setup-nodewrites a literal${NODE_AUTH_TOKEN}reference to.npmrc, not the resolved token. Disk-scanning worms find only the variable name. Replacing changesets/action with a custom publish script is a larger change with marginal benefit onceNODE_AUTH_TOKENscoping (a25f53d) lands.pnpm.verifyAttestations: true— pnpm v9 has no equivalent config.npm audit signaturescannot readpnpm-lock.yaml. The post-publishaudit signaturescheck (9494677) covers the outgoing side.pnpm.minimumReleaseAge(registry cooldown) — pnpm v10+ feature. Tracked as follow-up alongside the v10 upgrade.egress-policy: block— currentlyauditeverywhere. Flipping toblockneeds ~2 weeks of audit data first.Risks introduced
ajvhad to be force-pinned via three parent-scoped overrides (eslint>ajv,@eslint/eslintrc>ajv,@microsoft/tsdoc-config>ajv) to keep ESLint 7 and tsdoc-config working on ajv@6 while the rest of the tree resolves ajv@8. If a new transitive consumer of ajv@6 appears, OSV-Scanner will catch it and another parent-scoped entry needs to be added.diffjumps from older majors → 9.x.basic-ftp5.x → 6.x is a major bump.gotjumps 9.x → 11.x (deliberately not 14.x/15.x to minimise transitive consumer breakage).tough-cookiejumps 2.x → 4.x.If any consumer breaks at install time, the override range can be widened or scoped to a specific parent.
Manual follow-ups (out of scope)
mainto actually enforce CODEOWNERS reviewsocket.ymlactually enforces on PRsegress-policyfromaudittoblockwith an explicit allowlistminimumReleaseAge(registry cooldown) and the v10-default install-script allowlistrequesttransitive (GHSA-p8p7-x288-28g6) by finding the consumer that still depends on it and migrating it togot/undici/ native fetchignoreUntildate inosv-scanner.tomlexpires (2026-08-13 for elliptic + showdown, 2026-11-13 for request)Test plan
pnpm install --frozen-lockfileworks locallypnpm prepare, husky hooks fire on commitpnpm build,pnpm test,pnpm lintstill passosv-scanner.toml)router_init.js, no@tanstack/setuplockfile reference, no persistence artefacts)sbom-cyclonedxis uploaded by validatenpm audit signaturesverifies provenancesocket.ymlis recognized`