Skip to content

chore(security): harden supply chain against postinstall-worm attacks - #167

Merged
Aidosmf merged 52 commits into
mainfrom
mf/security-improvements
May 19, 2026
Merged

chore(security): harden supply chain against postinstall-worm attacks#167
Aidosmf merged 52 commits into
mainfrom
mf/security-improvements

Conversation

@Aidosmf

@Aidosmf Aidosmf commented May 13, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. Repo + CI hardening (workflows, configs, policies)
  2. Metadata + Socket.dev hygiene
  3. Dependabot CVE remediation (16 alerts closed)
  4. Mini Shai-Hulud detection layer (per StepSecurity advisory)
  5. OSV-Scanner remediation (4 patches + 3 documented suppressions)

Each fix is a single self-contained commit so reviewers and bisect
can isolate them.

Refs:

HIGH severity — install-time and CI-token vectors

  • 21b8ddb Add .npmrc with ignore-scripts=true — blocks dependency lifecycle scripts during pnpm install (primary worm vector)
  • e70a2f6 Pin all GitHub Actions to 40-char commit SHAs — defeats the tj-actions/changed-files-style mutable-tag compromise
  • f8f4880 Add --frozen-lockfile --ignore-scripts to every CI install
  • dc189c6 Set persist-credentials: false on every actions/checkout — keeps GITHUB_TOKEN out of .git/config
  • a25f53d Drop NODE_AUTH_TOKEN from GHPR install step — token now only in publish step env

MEDIUM severity — defense in depth

  • 23c77e6 Pin packageManager with sha512 integrity hash
  • 963a745 Mark monorepo root "private": true
  • 1d926aa Add empty pnpm.onlyBuiltDependencies allowlist
  • 2c80f2e Add pnpm audit --prod --audit-level=high to validate
  • 514570a Add gitleaks secret-scan workflow (direct binary, sha256 verified)
  • fe76c88 Add StepSecurity Harden-Runner to every CI job (audit mode)
  • 397b54a Add .github/CODEOWNERS requiring @LottieFiles/rnd review on supply-chain-sensitive paths
  • b0afe39 Set workflow-level permissions: {} (deny-by-default) on release.yml and codeql.yml
  • 1901f3b Add .github/dependabot.yml with groups: for npm + github-actions
  • 9494677 Post-publish npm audit signatures verifies sigstore attestation on each new release
  • 00e34a3 Add publishConfig with provenance: true to relottie-stringify (only workspace missing it)
  • d6a2ced Pin the last two floating production dep ranges (unified-args, filesize) to exact versions
  • afe6937 Add socket.yml policy mapping worm-class alerts to error (enables Socket GitHub App)

LOW severity / docs

  • d83a101 Add SECURITY.md with private disclosure policy
  • 55ca018 Add CodeQL workflow (security-and-quality query pack)
  • d551bd0 Pin Node to lts/iron via .nvmrc
  • 50b633b Generate and upload CycloneDX SBOM as workflow artifact
  • 9d56721 Require engines.npm: ">=9.5.0" on all 8 publishable workspace packages
  • c46a19b Fix broken homepage URLs in three packages
  • e18daa8 Document new CI workflows and supply-chain policies in README

Mini 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:

  • 72de359 Advisory layer: OSV-Scanner reusable workflow cross-references pnpm-lock.yaml against the OSV vulnerability database
  • f461ab3 File-system layer: ioc-scan CI job installs deps with --ignore-scripts then greps the tree for the exact IoCs (router_init.js, tanstack_runner.js, @tanstack/setup@github: lockfile pattern, ransom-marker token description, persistence artefacts)
  • 5023c2a Human layer: SECURITY.md self-detection runbook with 4 copy-paste local-check commands and the critical "do NOT revoke npm tokens before imaging the disk" warning
  • 1f2f895 Fix: ioc-scan excludes self-referential docs from the ransom-marker grep

Dependabot CVE remediation — 16 alerts closed

Commit Override Closes
398877e basic-ftp ^6.0.1 118, 121, 123, 127 (4× HIGH)
4f9dcbc lodash ^4.18.1 119 HIGH, 120 MOD
ae44ae4 fast-uri ^3.1.2 128, 129 (2× HIGH)
48a32f6 protocol-buffers-schema ^3.6.1 122 MOD
e251d75 + b68952e ajv parent-scoped to ESLint chain 90 MOD (initial global bump broke ESLint 7; second commit narrows to parent-scoped form)
86d942c showdown ^2.1.0 125 MOD
d08b4c5 ip-address ^10.2.0 126 MOD
844067c elliptic ^6.6.1 #75 LOW
1dd566c diff ^9.0.0 82, 78 (2× LOW, duplicates)
b83d2e9 tmp ^0.2.5 tmp symlink-write LOW
ef6f526 regenerate pnpm-lock.yaml enables all of the above

OSV-Scanner remediation — 7 advisories, all triaged

Commit Action OSV ID
391f7f9 Bump ajv ESLint-scoped override 6.12.6 → 6.14.0 GHSA-2g4f-4pwh-qvx6
2cfe876 Add d3-color ^3.1.0 GHSA-36jr-mh4h-2g58
f4bf6b6 Add got ^11.8.5 GHSA-pfrx-2q88-qq97
d5299c5 Add tough-cookie ^4.1.3 GHSA-72xf-g2v4-qvf3
1c9f14b osv-scanner.toml suppresses 3 unfixable advisories with ignoreUntil dates GHSA-848j-6mx2-7j84 (elliptic, latest), GHSA-p8p7-x288-28g6 (request, deprecated), GHSA-rmmh-p597-ppvv (showdown, latest)
ade4799 Extend ajv override to @microsoft/tsdoc-config parent + lockfile regen follow-up

Net result: 4 patched, 3 suppressed with documented re-check dates, 0 unfixed without justification. pnpm run lint continues to pass (0 errors).

Breaking change for contributors

ignore-scripts=true in .npmrc means husky's prepare no longer runs automatically. After cloning, run pnpm prepare once. README updated.

Intentionally skipped (with reasoning)

  • Ephemeral npm credential pattern (mktemp + trap)actions/setup-node writes 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 once NODE_AUTH_TOKEN scoping (a25f53d) lands.
  • pnpm.verifyAttestations: true — pnpm v9 has no equivalent config. npm audit signatures cannot read pnpm-lock.yaml. The post-publish audit signatures check (9494677) covers the outgoing side.
  • pnpm.minimumReleaseAge (registry cooldown) — pnpm v10+ feature. Tracked as follow-up alongside the v10 upgrade.
  • Harden-Runner egress-policy: block — currently audit everywhere. Flipping to block needs ~2 weeks of audit data first.

Risks introduced

  • ajv had 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.
  • diff jumps from older majors → 9.x.
  • basic-ftp 5.x → 6.x is a major bump.
  • got jumps 9.x → 11.x (deliberately not 14.x/15.x to minimise transitive consumer breakage).
  • tough-cookie jumps 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)

  • Configure branch protection on main to actually enforce CODEOWNERS review
  • Disable CodeQL "default setup" in Settings → Code security so the SHA-pinned workflow can upload SARIF (currently they conflict)
  • Enable Private Vulnerability Reporting in repo settings
  • Install the Socket Security GitHub App so socket.yml actually enforces on PRs
  • After ~2 weeks of Harden-Runner audit data, flip egress-policy from audit to block with an explicit allowlist
  • Plan a pnpm v9 → v10 upgrade to unlock minimumReleaseAge (registry cooldown) and the v10-default install-script allowlist
  • Drop the deprecated request transitive (GHSA-p8p7-x288-28g6) by finding the consumer that still depends on it and migrating it to got / undici / native fetch
  • Re-run OSV-Scanner after each ignoreUntil date in osv-scanner.toml expires (2026-08-13 for elliptic + showdown, 2026-11-13 for request)
  • Sign in to socket.dev to review the bottom-5 transitive deps by score

Test plan

  • CI passes on this PR (validate + security 3-job + codeql workflows)
  • pnpm install --frozen-lockfile works locally
  • After pnpm prepare, husky hooks fire on commit
  • pnpm build, pnpm test, pnpm lint still pass
  • gitleaks workflow scans full history without flagging false positives
  • OSV-Scanner job exits clean (4 fixed, 3 suppressed via osv-scanner.toml)
  • IoC scan job exits clean (no router_init.js, no @tanstack/setup lockfile reference, no persistence artefacts)
  • CodeQL run completes and uploads SARIF (requires default-setup CodeQL disabled in repo settings)
  • SBOM artifact sbom-cyclonedx is uploaded by validate
  • Harden-Runner insights show no unexpected egress
  • Dependabot UI shows all 16 alerts closed after merge
  • On the next release, post-publish npm audit signatures verifies provenance
  • After Socket GitHub App is installed, socket.yml is recognized
    `

Aidosmf added 21 commits May 13, 2026 16:37
…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.
@Aidosmf Aidosmf self-assigned this May 13, 2026
@changeset-bot

changeset-bot Bot commented May 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 65072f3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Aidosmf added 7 commits May 13, 2026 21:40
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.
Aidosmf added 15 commits May 13, 2026 22:00
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.
@github-advanced-security

Copy link
Copy Markdown
Contributor

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:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Aidosmf added 9 commits May 13, 2026 22:34
…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.
@Aidosmf
Aidosmf merged commit 869db2c into main May 19, 2026
9 checks passed
@Aidosmf
Aidosmf deleted the mf/security-improvements branch May 19, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants