Skip to content

RFC: package-level install trust check - #916

Open
davidnichols-ops wants to merge 3 commits into
npm:mainfrom
davidnichols-ops:package-install-trust-check
Open

RFC: package-level install trust check#916
davidnichols-ops wants to merge 3 commits into
npm:mainfrom
davidnichols-ops:package-install-trust-check

Conversation

@davidnichols-ops

@davidnichols-ops davidnichols-ops commented Aug 19, 2026

Copy link
Copy Markdown

Summary

This RFC adds an opt-in, per-package trust gate at the registry tarball endpoint.
Publishers choose one of four modes via publishConfig.trustCheck:

Mode Behavior
none (default) No change — tarballs served freely
audit Log every fetch with a trust classification; never block
require Accept heuristic automation (zero-config CI pass) or hardware-bound human (WebAuthn / biometric) tokens
require-human Accept only hardware-bound human tokens; CI must use explicit automation/service tokens or OIDC trusted publishing

Three traffic lanes, zero CAPTCHA:

  1. Heuristic automation pass — CI and headless runners get a frictionless tarball. The registry classifies requests using passive signals (known CI ASN/IP ranges, User-Agent/TLS fingerprints, an echoed X-NPM-Trust-Check-Challenge nonce). No tokens, no browser, no setup.
  2. Hardware-bound human check — Residential IPs, unknown clients, and ambiguous traffic hit a 428 Precondition Required. The npm CLI triggers a native OS biometric prompt (Touch ID, Windows Hello, security key) via WebAuthn with userVerification=required. The signature is bound to npmjs.com and the package name; a remote LLM in a cloud sandbox cannot touch the enclave.
  3. Hard spam_user block — Known scrapers, TOR exits, open proxies, exploit tooling, and repeat abusers get 403 TRUST_CHECK_BLOCKED immediately. No challenge, no fallback, no CAPTCHA.

Every fetch (allowed or blocked) emits a standardized install-trust-check event with a tokenKind (heuristic-automation, unattended, human, automation, service, or spam_user), giving publishers real visibility into human vs automated adoption — something per-package CI tokens never delivered.

Why this approach

  • Per-package CI tokens do not scale and still leave publishers blind to human vs automated traffic.
  • CAPTCHAs are defeated by LLM computer-use and vision models, add latency, and hurt accessibility.
  • The registry is the only point that can enforce the policy before the tarball is served without modifying package contents.

Files added

  • accepted/0000-package-install-trust-check.md — the proposal
  • accepted/assets/package-install-trust-check/trust-check-ui-mockup.html
  • accepted/assets/package-install-trust-check/trust-check-ui-mockup.png
  • accepted/assets/package-install-trust-check/trustcard-logo.png

Validation passed:

node bin/rfc-validate.js accepted/0000-package-install-trust-check.md
# rfc-validate: OK (0 warning(s))

All RFC-required sections are present and no template placeholders remain.

@davidnichols-ops
davidnichols-ops requested review from a team as code owners August 19, 2026 01:31
@devin-ai-integration
devin-ai-integration Bot force-pushed the package-install-trust-check branch from 8776f84 to 96753a3 Compare August 19, 2026 01:45
@ljharb

ljharb commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The information about how a package was published is largely secret, intentionally so, so I'm not sure how this would be enforced.

Separately, trusted publishing is simply not a security indicator of any kind, and using it in any such policy is a category error.

@davidnichols-ops
davidnichols-ops force-pushed the package-install-trust-check branch from fc2a7aa to c0971d4 Compare August 19, 2026 02:52
@davidnichols-ops
davidnichols-ops force-pushed the package-install-trust-check branch from c0971d4 to c019e97 Compare August 19, 2026 02:53
@davidnichols-ops

Copy link
Copy Markdown
Author

Deep Technical Review: Package-level install trust check for automated and human consumers

Verdict

Ship audit everywhere. Do not ship require/require-human without session/replay hardening and a publish-side threat-model alignment.

What's Well Done

  • Killing CAPTCHA is correct. WebAuthn assertions are phishing-resistant, bound to origin and challenge, and never transmit biometric data — the fingerprint unlocks a private key that stays on-device. This is the right primitive.
  • Zero-friction lane for recognized CI is the right instinct. Routing CI through OIDC/trusted-publisher machinery npm already shipped (rather than inventing new plumbing) is sensible reuse.
  • audit mode is free intelligence. Every fetch becomes a classified, logged event with tokenKind — no blocking, no false positives. This is the only mode that should ship unconditionally.

Where the Threat Model Misses

The two incidents that defined npm security in the past year — Shai-Hulud (Sept 2025, 500+ packages via postinstall token theft) and the chalk/debug phished-maintainer takeover (17 packages, 2B+ weekly downloads) — were both publish-side compromises:

  • Compromised maintainer tokens / phished accounts published poisoned versions
  • The tell was lost provenance attestation on the new versions, not unusual download traffic
  • Install traffic was completely ordinary: real developers and real CI pulling routine dependency bumps

npm and GitHub's actual hardening this year has correctly landed on the publish side:

  • Mandatory WebAuthn-based 2FA for writes
  • OIDC trusted publishing instead of long-lived tokens
  • Staged publishing demanding proof-of-presence on every publish, including CI/CD
  • The parallel install-scripts-opt-in RFC cuts off the postinstall hook the worm used

A download-side "prove you're human/CI" gate does not touch that chain. The worm runs after a legitimate install already succeeded, on hardware that already proved it was human. If require-human caches that proof into a session/token rather than re-prompting on every fetch (the RFC doesn't specify which), that cached credential becomes one more thing for this exact class of malware to steal — notable given April 2025's strain that planted a malicious MCP server + prompt injection into Claude Code/Cursor to harvest stored keys.

Second-Order Risks Introduced

  1. Credential caching becomes a target. If human tokens are session-cached (likely, for UX), the same malware that steals npm tokens and SSH keys now has a new high-value target: the trust-check token cache at ~/.npm/_trust-checks.
  2. Heuristic automation ≠ proof. Known-CI-ASN + fingerprint is exactly the profile a compromised CI job already has — it genuinely is CI. This lane provides convenience, not security.
  3. Hardware-binding raises cost, doesn't eliminate automation. Mechanical USB security-key pressers (documented click farms for near-identical schemes) mass-produce valid WebAuthn signatures. Cloudflare's own writeup concedes a mechanical actuator passes the check. The RFC's "no remote LLM can touch the enclave" framing overclaims.
  4. Ecosystem friction. pnpm's much lighter trust-downgrade check (no biometrics) already generates steady "breaking installs, how do I turn this off" reports. A hardware-bound biometric prompt is a heavier ask by a wide margin, landing hardest on headless servers, SSH sessions, and developers without security keys.

Mode-by-Mode Assessment

Mode Assessment Reasoning
none Neutral Default, no change
audit Ship everywhere Free structured telemetry; zero false-positive cost; early warning for agent-hallucination install storms (e.g., researcher finding agents daily trying to install a hallucinated package months later)
require Do not ship Heuristic lane provides no cryptographic assurance; human lane caches credentials malware already targets; blocks legitimate headless/SSH workflows
require-human Do not ship Same risks as require, plus breaks all unattended CI unless publishers manually provision automation/service tokens — which is exactly the per-package token management the RFC claims to solve

Concrete Recommendations

  1. Ship audit as the only default-on mode. Make it opt-out, not opt-in. The telemetry value (human vs automation adoption signal) is the genuine contribution here.
  2. Keep require/require-human as opt-in experimentation only, behind a feature flag, with:
    • Explicit session/replay semantics documented: does a human token cache? TTL? Scope? Can it be replayed from a compromised machine?
    • Re-prompt policy: every fetch vs first fetch per session vs first fetch per package version
    • Threat-model doc showing how this addresses publish-side compromise (it doesn't — be honest about that)
  3. Add a tokenKind: "compromised-publish" signal to the audit event when a package version lacks provenance attestation that prior versions had — this would actually catch the Shai-Hulud/chalk pattern at install time.
  4. Do not cache human tokens across package boundaries. If a human verifies for package A, that proof should not automatically satisfy package B's require-human gate. Cross-package token reuse is exactly how malware would pivot.

Open Questions Requiring Answers Before Merge

  • What is the exact TTL and scope of human / heuristic-automation / unattended tokens? Package-scoped? Session-scoped? User-scoped?
  • Does a successful human verification cache a token that subsequent npm install calls reuse without re-prompting? If yes, what prevents malware from extracting and replaying that token?
  • Why does the threat model focus on download-side abuse (scrapers, download-count inflation) when 2024-2025's catastrophic incidents were all publish-side?
  • Has the mechanical WebAuthn actuator attack been modeled? What's the cost-per-thousand-signatures for a click-farm vs a CAPTCHA farm?
  • What's the fallback for developers on headless servers, SSH, or without security keys? The email fallback is two clicks but requires a verified email — what about throwaway CI containers?

Review by nillmore — reasoning via deep-technical-reviewer agent

@davidnichols-ops

Copy link
Copy Markdown
Author

slap happy agents be tweakin...

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.

2 participants