Skip to content

[BUG] Trusted publishing auto-provenance silently discards the --provenance-file bundle #9879

Description

@yunseo-kim

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

  1. lib/commands/publish.js calls oidc() unconditionally before reading credentials
    (L145-L155). The OIDC flow auto-enables provenance when the config is at its default
    (lib/utils/oidc.js L144-L168: config.isDefault('provenance')
    opts.provenance = true).
  2. In libnpmpublish/lib/publish.js buildMetadata() (L145-L179), the inner
    if (provenance === true) branch runs generateProvenance(), so the inner else
    (verifyProvenance(subject, provenanceFile), the only code path that reads the
    supplied file) never executes.
  3. 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-file is set via CLI (the where === '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=false in user/global npmrc: throws the same exclusive-config error.
  • publishConfig.provenance=false: the manifest flatten happens after oidc()
    runs (commands/publish.js L248-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

  1. 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.
  2. 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).
  3. Run: npm publish <pkg>.tgz --provenance-file=<bundle>.json --registry=https://registry.npmjs.org
    (npm 11.5.1+; verified through 12.0.2).
  4. After publish, fetch
    https://registry.npmjs.org/-/npm/v1/attestations/<url-encoded-pkg>@<version>,
    select the https://slsa.dev/provenance/v1 entry, and base64-decode
    .bundle.dsseEnvelope.payload.
  5. 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:
    ; 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.
    Note: the original failure occurred inside a reusable workflow that invokes npm
    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bugthing that needs fixingPriority 2secondary priority issuecmd:publishrelated to `npm publish`

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions