Repository navigation
[BUG] Trusted publishing auto-provenance silently discards the --provenance-file bundle #9879
Description
Is there an existing issue for this?
- I have searched the existing issues
This issue exists in the latest npm version
- I am using the latest npm
This is not just a request to bump a dependency for a CVE
- This is not solely a request to bump a dependency for a CVE
Current Behavior
When publishing with an externally generated provenance bundle under OIDC trusted
publishing (npm publish <tarball> --provenance-file=<bundle> from GitHub Actions,
public repository, public package), npm silently discards the supplied bundle and
publishes its own auto-generated provenance instead.
Mechanism (npm 12.0.2, commit b888cc9):
lib/commands/publish.jscallsoidc()unconditionally before reading credentials
(L145-L155). The OIDC flow auto-enables provenance when the config is at its default
(lib/utils/oidc.jsL144-L168:config.isDefault('provenance')→
opts.provenance = true).- In
libnpmpublish/lib/publish.jsbuildMetadata()(L145-L179), the inner
if (provenance === true)branch runsgenerateProvenance(), so the innerelse
(verifyProvenance(subject, provenanceFile), the only code path that reads the
supplied file) never executes. - The registry therefore receives npm's generated statement (builder.id =
https://github.com/actions/runner/github-hosted,internalParameters.github,
sha512-only subject) in place of the supplied one. No warning or error is emitted;
the publish "succeeds".
The same structure exists in v11.5.1 (commit da1d4d2), so the whole trusted-publishing
release line is affected.
Additionally, every documented way to disable automatic provenance is unusable in
combination with --provenance-file:
NPM_CONFIG_PROVENANCE=false: on 12.0.2 the env value is silently skipped when
--provenance-fileis set via CLI (thewhere === 'env'carve-out in
@npmcli/config#loadObject); on 11.5.1 the same combination throws
--provenance can not be provided when using --provenance-file.provenance=falsein user/global npmrc: throws the same exclusive-config error.publishConfig.provenance=false: the manifest flatten happens afteroidc()
runs (commands/publish.jsL248-L280 vs L145-L155), so it cannot prevent the
auto-enable.
Expected Behavior
The --provenance-file documentation states: "When publishing, the provenance bundle
at the given path will be used."
Expected: an explicitly supplied --provenance-file takes precedence over the implicit
OIDC auto-provenance (the supplied bundle is verified via verifyProvenance() and
published), or, if the combination is considered invalid, npm rejects it explicitly
with a clear error. npm must not silently discard a caller-supplied, cryptographically
meaningful artifact. The trusted-publishing documentation should also state how
automatic generation interacts with --provenance-file.
Steps To Reproduce
- Environment: GitHub Actions workflow on a public repository, with
permissions: id-token: write, and an npm trusted publisher configured for the
repository + workflow filename. Target package is public. - Config: no provenance settings anywhere (defaults); generate or obtain a valid
Sigstore provenance bundle for the tarball by external means (the documented purpose
of--provenance-file, e.g. PR feat: add --provenance-file flag for publish command #6490). - Run:
npm publish <pkg>.tgz --provenance-file=<bundle>.json --registry=https://registry.npmjs.org
(npm 11.5.1+; verified through 12.0.2). - After publish, fetch
https://registry.npmjs.org/-/npm/v1/attestations/<url-encoded-pkg>@<version>,
select thehttps://slsa.dev/provenance/v1entry, and base64-decode
.bundle.dsseEnvelope.payload. - Observe: the statement is npm's generated shape
(externalParameters.workflow,internalParameters.github,
builder.id = https://github.com/actions/runner/github-hosted, sha512-only subject),
not the supplied statement. The supplied bundle was never read.
Note: npm publish --dry-run does not invoke libnpmpublish's provenance path, so this
is only observable on a real publish — a disposable scoped test package is recommended.
Real-world evidence: observed during a live publish of @windlass/vers-js@0.1.2
(workflow run: https://github.com/windlasstech/vers-js/actions/runs/31840088262); the
registry's SLSA provenance payload differs byte-for-byte from the bundle that was
passed via --provenance-file (preserved build artifact available on request).
Environment
- npm: 11.17.0 (observed live); code path verified identical in 11.5.1 and 12.0.2 sources
- Node.js: v24.19.0
- OS Name: Ubuntu 24.04.4 LTS (GitHub-hosted runner)
- System Model Name: GitHub Actions
ubuntu-24.04 - npm config:
Note: the original failure occurred inside a reusable workflow that invokes npm
; node bin location = /opt/hostedtoolcache/node/24.19.0/x64/bin/node ; node version = v24.19.0 ; npm local prefix = /home/runner/work/vers-js/vers-js ; npm version = 11.17.0 ; cwd = /home/runner/work/vers-js/vers-js ; HOME = /home/runner ; Run `npm config ls -l` to show all defaults.
with stronger isolation (stripped NPM_CONFIG_* env, empty user/global
config files, isolated temp cwd) — so ambient configuration cannot explain the
substitution. The reusable-workflow indirection does not affect the bug: npm still
runs on a GitHub-hosted runner with GITHUB_ACTIONS=true and OIDC request variables
present, which is all the auto-enable path requires.
Full downstream root-cause analysis, including a byte-level comparison of the
supplied bundle versus the statement actually published by the registry:
windlasstech/slsa-builder#97
Activity
fix: provenance-file takes precedence over OIDC auto-generated proven…