Supply-chain security
Think of it like… the tamper-evident seal on every ingredient that enters a restaurant's kitchen. The seal proves nobody opened the jar between the farm and the chef (--provenance). The 24-hour fridge quarantine means an obviously-poisoned batch gets recalled before it's served (minimum-release-age=1440). And refusing to run the "please install this companion app" pop-up that ships with the package keeps malware out of the prep area (ignore-scripts=true).
npm worm campaigns ship in waves: chalk/debug in September 2025, node-ipc in May 2026, the @tanstack/* compromise on 2026-05-11. The pattern is consistent: a single phished maintainer or one CI cache-poisoning bug becomes thousands of downstream installs in minutes. DaloyJS is built and shipped with that threat model in mind, and we recommend the same defaults for your project.
How DaloyJS itself is published
- 01triggerSigned tag pushrelease.yml only; fork PRs can't
- 02gateProtected environmentmaintainer approval required
- 03runnerharden-runner egress blocknpm + GitHub + Sigstore only
- 04authOIDC trusted publishno long-lived NPM_TOKEN
- 05attest--provenance via Sigstoretarball bound to commit + run
- Releases run in a separate workflow (
release.yml) that is triggered only by a signed tag push and gated by a protected GitHub Environment requiring maintainer approval. Fork PRs cannot touch it. - npm trusted publishing (OIDC) with
--provenance: every@daloyjs/coretarball is bound to its source commit and workflow run via Sigstore. There is no long-livedNPM_TOKENin repo secrets to steal. id-token: writeis granted only to the publish job, on the post-approval runner, with egress blocked to everything except npm, GitHub, and Sigstore (viastep-security/harden-runner).- No GitHub Actions cache in the standard CI workflow. Cache scope bridges fork PRs and pushes to
main, which is the poisoning channel that bridged TanStack's PR pipeline into its release pipeline. - No
pull_request_targetthat runs fork code. CI uses the safepull_requesttrigger; the one narrow exception (a workflow that auto-closes external PRs) never checks out, installs, or runs any PR code. Azizmorcheck on every PR fails the build on the dangerouspull_request_target-plus-fork-checkout pattern. - Third-party GitHub Actions are SHA-pinned so a retargeted version tag cannot silently change what CI executes.
- CodeQL, OpenSSF Scorecard, Dependabot all run continuously, and
CODEOWNERSblocks any change to.github/,package.json, the lockfile, or.npmrcwithout a maintainer review. - ClusterFuzzLite continuously fuzzes the untrusted-input parsers with Jazzer.js, on every PR that touches
src/and again in a daily batch run (see below). - Lockfile source verification runs in CI via
pnpm verify:lockfileand fails ifpnpm-lock.yamlintroduces git dependency sources or non-registry tarball URLs.
Full policy and incident-response playbook: SECURITY.md.
How the framework itself is fuzzed
Beyond static analysis, the untrusted-input parsers in @daloyjs/core are continuously fuzzed with Jazzer.js wired through ClusterFuzzLite. A per-PR code-change run fuzzes anything that touches src/, and a daily batch job fuzzes the full corpus. This is also what earns the OpenSSF Scorecard Fuzzing check.
Each target asserts the function's documented contract, not just "does not crash". A declared rejection (for example a BadRequestError on malformed input) is correct behavior and is ignored; any other thrown error, or a hang, is a finding:
safeJsonParse: only throwsBadRequestError, and never returns an object carrying a__proto__/constructor/prototypeown key.readRequestCookie: never throws while parsing an untrustedCookieheader.decodeCursor: only throwsBadRequestErroron a malformed pagination cursor.parseCron: only throwsCronParseError.parseIp: never throws (returnsundefinedon unrecognized input).sanitizeHeaderName/sanitizeHeaderValue: only throwBadRequestError, and an accepted value never contains CR, LF, or NUL.
The harness, including the per-target oracle and the digest-pinned OSS-Fuzz build image, lives in .clusterfuzzlite/.
Defaults you get from pnpm create daloy
Every project scaffolded with create-daloy ships with an.npmrc and pnpm-workspace.yaml that turn on the install-time controls below when you choose pnpm. Keep them on.
Optional CI bundle for user projects
create-daloy --with-ci adds the GitHub-side controls that do not come from a package install: CI with top-level permissions: {}, SHA-pinned actions,harden-runner, no package-manager cache, disabled lifecycle scripts, lockfile-source verification, CodeQL, OpenSSF Scorecard, zizmor, Dependabot, CODEOWNERS, and SECURITY.md. Templates can also get a manual-only deploy.ymlstarter: container templates publish a Docker image to GHCR, while Vercel and Cloudflare templates run their platform CLIs with credentials from GitHub Actions secrets and variables. The scaffolder deliberately omits npm publishing workflows because generated projects are REST API services, not reusable libraries.
GitHub settings are still your responsibility: replace the CODEOWNERS owner if needed, enable branch protection, require the generated checks, and turn on secret scanning plus push protection.
On GitLab, Bitbucket, Azure DevOps, Jenkins, or an on-prem runner, you still inherit the runtime guardrails, @daloyjs/core's zero-runtime-dependency package, SBOM and npm provenance, and the pnpm install-time controls if you choose pnpm. Translate the GitHub workflow rules above into your CI host: start from no default write permissions, avoid shared dependency caches for untrusted code, keep installs reproducible, and isolate any job that can publish or deploy.
If you legitimately need a postinstall
ignore-scripts=true is global. To allow a build script for a package you actually trust (e.g. esbuild), allowlist it explicitly in package.json:
DaloyJS itself uses the pnpm 11+ equivalent, an allowBuilds allowlist in pnpm-workspace.yaml (package.json#pnpm.onlyBuiltDependencies is the pre-v11 form). Each entry should be reviewed in PR.
Avoid git and tarball dependencies
DaloyJS also checks its root lockfile for dependency sources that bypass the normal npm registry path. In this repo, pnpm verify:lockfile fails on git dependencies and non-registry tarball URLs so a transitive source change cannot slip through as ordinary version churn.
Optional: install-time malware scanners
The 24-hour minimum-release-age cooldown is what bridges the gap between a malicious version being published and the registry yanking it. Aikido's “quantum incident response” write-up makes the point that you cannot out-react an npm worm once it lands: prevention at install time is the only viable defense. DaloyJS's install defaults already implement that thesis (cooldown, no transitive lifecycle hooks, zero runtime deps in @daloyjs/core, frozen + verified store). For belt-and- braces beyond the cooldown, install a real-time scanner that intercepts package-manager calls and checks each requested version against a live malware feed before it touches disk:
DaloyJS deliberately does not add safe-chain (or any other third-party scanner) as a dependency or scaffold default. @daloyjs/core ships zero runtime dependencies by policy and any install-time tool you run is your trust decision, not the framework's. Equivalent commercial offerings (Socket, Snyk Advisor, JFrog Curation, npm's own Package Trust) sit at the same layer; pick one or run none, but understand that minimum-release-age=1440 is already doing most of the work the article recommends.
Mapped to the ENISA package-manager advisory
ENISA's Technical Advisory for Secure Use of Package Managers (v1.1, March 2026) is the EU reference checklist for consuming third-party packages, organised across a four-stage life cycle. DaloyJS implements the integration checklist as shipped defaults, including the two controls ENISA itself flags as “optional” or “more suited for high-security environments.”
- stage 1Selecttrustworthy, verified, maintained
- stage 2Integrateintegrity, source, scripts, pinning
- stage 3Monitorscan, track CVEs, ownership changes
- stage 4Mitigateassess, prioritise, patch, document
| ENISA recommendation | DaloyJS control |
|---|---|
Installation script prevention (ignore-scripts; ENISA flags as “high-security”) | Default, not opt-in: ignore-scripts=true + pnpm verify:no-lifecycle-scripts |
Release-age delay (--before; ENISA flags as “optional and situational”) | Standing policy: minimum-release-age=1440 (24h) |
| Integrity / lockfile verification (SHA-512) | frozen-lockfile + verify-store-integrity + pnpm verify:lockfile |
| Package source enforcement (trusted registry only) | registry= pinned + pnpm verify:lockfile |
| SBOM creation | CycloneDX 1.5 + SPDX 2.3 per release + pnpm verify:sbom |
| Trusted Publishing + provenance | OIDC trusted publishing (no long-lived token) + --provenance Sigstore |
| Internal allowlist of approved package names | pnpm verify:known-dep-names |
| Reduce dependencies (“is the dependency needed?”) | @daloyjs/core ships zero runtime deps (verify:no-runtime-deps) |
ENISA section 5.2 names slopsquatting (attackers pre-registering hallucinated package names that AI tools emit) as a first-class AI-era threat. DaloyJS closes both axes: verify:known-dep-names forces every top-level dependency onto an explicit allowlist (name axis), and minimum-release-age=1440 waits out the window in which a slop-squat is typically detected and unpublished (time axis). Full recommendation-by-control table: SECURITY.md → ENISA mapping.
ENISA section 5.2's concern is backed by survey data. Aikido's State of AI in Security & Development 2026 report (450 practitioners) found that 69% of organizations have uncovered vulnerabilities introduced by AI-generated code and 1 in 5 had a serious incident tied to it, and that automated CI gates reduce incidents while manual review and tool sprawl do not. That is the case for DaloyJS's posture here: secure-by-default output means AI-generated code starts safe, and the fail-closed verify:* gates are exactly the kind of automated, low-false-positive guardrail the report associates with fewer incidents.
What to do if a maintainer account is phished
The September 2025 chalk/debug compromise started with a single fake npmjs.help 2FA-reset email. If you suspect a maintainer (yours or an upstream's) was phished:
- Pin every direct dependency that lists the affected maintainer to the last known-good version in your lockfile.
pnpm audit --prodand rotate any deployment credential the install host had access to (npm token, GitHub token, AWS keys, SSH keys).- Bump
minimum-release-agein.npmrcfurther (e.g.4320for 72h) until the campaign settles. - Subscribe to GitHub Security Advisories for your dependency tree.
Hardening your own GitHub Actions
If you publish your own application's artifacts from CI, copy these rules:
- Never use
pull_request_targetto check out fork code. - Top-level
permissions: {}; opt back in per job. - Pin third-party actions to a commit SHA (Dependabot will keep them updated). A retargeted tag has the same blast radius as cache poisoning.
- Separate the publish job. Do not put
id-token: writeon a workflow that runs untrusted code in any earlier step. OIDC tokens have been pulled from runner memory in real attacks. - Use
step-security/harden-runneron the publish job withegress-policy: blockand an explicit allowlist. - Use a protected GitHub Environment (required reviewers) for any job that can publish.
Further reading
- Aikido: Quantum incident response: why traditional IR cannot catch an npm worm, and why install-time prevention (cooldowns, blocked scripts, malware-feed scanners) is the only viable defense.
- TanStack 2026-05-11 postmortem: the cache-poisoning + OIDC-extraction chain in detail.
- TanStack incident follow-up: what they changed afterwards.
- GitHub Security Lab: preventing pwn requests.
- npm provenance documentation.
- ENISA: Technical Advisory for Secure Use of Package Managers (March 2026), and Socket's summary.
- Aikido: State of AI in Security & Development 2026 (survey of AI-generated-code incident rates).