Skip to content

Add --publish-only gate to narrow NPM_TOKEN exposure#16

Merged
jgowdy-godaddy merged 1 commit intomainfrom
feat/publish-only-gate
Apr 17, 2026
Merged

Add --publish-only gate to narrow NPM_TOKEN exposure#16
jgowdy-godaddy merged 1 commit intomainfrom
feat/publish-only-gate

Conversation

@jgowdy-godaddy
Copy link
Copy Markdown
Contributor

Summary

Addresses the number-one residual risk in `THREAT_MODEL.md`: every `npmenc` invocation injects `NPM_TOKEN*` into npm's environment by default, which npm then inherits to every lifecycle script (`preinstall` / `install` / `postinstall`) of every transitive dependency. A malicious transitive dep can `console.log(process.env.NPM_TOKEN_*)` and exfiltrate the token during a routine `npm install`.

Add an opt-in `--publish-only` flag that strips `NPM_TOKEN*` from the child environment unless the subcommand is one that actually authenticates to the registry:

```
publish, unpublish, deprecate, undeprecate,
access, owner, team, dist-tag, dist-tags,
whoami, profile, token, hook, hooks, org,
adduser, add-user, login, signup, logout,
star, unstar, stars
```

For those commands tokens still flow as before. For `install`, `ci`, `run-script`, `version`, etc., no token is set — the user explicitly trades away private-registry reads to close the exposure window on the commands most likely to execute untrusted JavaScript.

Detection heuristic

Positive scanner walks `args` left-to-right; the first arg matching a known auth or non-auth subcommand wins. Unknown args (likely flag values) are skipped. An unrecognized / custom subcommand defaults to "needs auth" to avoid silently breaking user workflows.

The known limitation: if a flag value happens to collide with a subcommand name (e.g. `--loglevel info` where `info` is an alias for `view`), the scanner may find the value first. Users can avoid this by writing `--flag=value` form. Covered by test `needs_auth_ambiguous_flag_value_collides_with_subcommand_name`.

Scope

  • `npmenc --publish-only ...` — new opt-in flag.
  • `npxenc` does NOT get the flag: npx invokes arbitrary package code whose registry-auth needs we can't predict. Always injects.
  • Threat model section "NPM_TOKEN_* injection when it is not needed" rewritten from "tracked hardening work" to describing the live mitigation.

Test plan

  • `cargo test --workspace` — 250 tests pass (196 in npmenc-core including 8 new gate tests, 26 in npmenc, 30 in libenclaveapp transitive, etc.).
  • `cargo clippy --workspace --all-targets -- -D warnings` clean.
  • `cargo fmt --all -- --check` clean.
  • CI green on macOS / Linux / Windows.

By default every npmenc invocation injects NPM_TOKEN* into the npm
process's environment, which npm then inherits to every lifecycle
script (preinstall / install / postinstall) of every transitive
dependency. That's the Type-2 lifecycle-script risk called out as the
number-one residual in THREAT_MODEL.md.

Add an opt-in --publish-only flag that strips NPM_TOKEN* from the
child env unless the subcommand is one that actually authenticates to
the registry (publish, unpublish, deprecate, access, owner, team,
dist-tag, whoami, profile, token, hook, org, adduser, login, signup,
logout, star). For those commands tokens still flow as before. For
install/ci/run-script/version/etc. no token is set — trading away
private-registry reads to close the exposure window on the commands
most likely to execute untrusted JavaScript with the token in scope.

Subcommand detection uses a positive scanner: args are walked left to
right; the first one matching a known auth or non-auth subcommand
wins. Unknown args are treated as flag values and skipped. An
unrecognized / custom subcommand defaults to 'needs auth' to avoid
silently breaking user workflows. Ambiguous cases where a flag value
collides with a subcommand name (e.g. --loglevel info) are documented
— users can avoid the edge by writing --flag=value.

npxenc does not expose --publish-only: npx invokes package code whose
registry-auth needs we can't predict, so it always injects.

Tests cover the positive / negative subcommand matching, flag-value
skipping, empty/unknown fallback, npx always-true, and the strip
helper's NPM_TOKEN prefix filter. THREAT_MODEL.md 'injection when not
needed' section updated to describe the mitigation and document the
known limitation.
@jgowdy-godaddy jgowdy-godaddy merged commit 2ee3733 into main Apr 17, 2026
3 checks passed
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