From 24f84374b5cc7bffe09bf061cb74cac60b56ce97 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 3 Mar 2026 22:47:53 +0000 Subject: [PATCH] cargo: strip credential-provider keys from .cargo/config.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem PR #14340 set `CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=""` to disable Cargo's credential lookup globally. However, per-registry `credential-provider` settings in .cargo/config.toml override the global env var: [registries.artifactory] credential-provider = "cargo:token" Cargo then invokes `cargo:token` for that registry, tries to look up `CARGO_REGISTRIES_ARTIFACTORY_TOKEN`, finds nothing, and fails with 'no token found'. ## Solution Parse .cargo/config.toml with TomlRB and strip `credential-provider` keys from both `[registries.*]` and `[registry]` sections before writing the config to the temporary working directory. Combined with the existing `CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=""` from #14340, this ensures Cargo never tries to authenticate on its own — all HTTP requests go through the dependabot proxy unauthenticated, and the proxy injects the real credentials transparently. Uses `is_a?(Hash)` guards rather than `T.let` casts so that unexpected config structure (valid TOML but not the shape we expect) is safely skipped rather than raising. Fixes #14354 --- .../cargo/file_updater/lockfile_updater.rb | 7 +- cargo/lib/dependabot/cargo/helpers.rb | 54 +++++++- .../cargo/update_checker/version_resolver.rb | 7 +- cargo/spec/dependabot/cargo/helpers_spec.rb | 125 ++++++++++++++++++ 4 files changed, 180 insertions(+), 13 deletions(-) diff --git a/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb b/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb index 21157d7186..c4cc450f50 100644 --- a/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb +++ b/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb @@ -304,10 +304,11 @@ def write_temporary_dependency_files File.write(lockfile.name, lockfile.content) File.write(T.must(toolchain).name, T.must(toolchain).content) if toolchain - return unless config + config_file = config + return unless config_file - FileUtils.mkdir_p(File.dirname(T.must(config).name)) - File.write(T.must(config).name, T.must(config).content) + FileUtils.mkdir_p(File.dirname(config_file.name)) + File.write(config_file.name, Helpers.sanitize_cargo_config(T.must(config_file.content))) end sig { void } diff --git a/cargo/lib/dependabot/cargo/helpers.rb b/cargo/lib/dependabot/cargo/helpers.rb index c409729235..70dfe3f8d8 100644 --- a/cargo/lib/dependabot/cargo/helpers.rb +++ b/cargo/lib/dependabot/cargo/helpers.rb @@ -1,21 +1,61 @@ -# typed: strong +# typed: strict # frozen_string_literal: true +require "toml-rb" + module Dependabot module Cargo module Helpers extend T::Sig + # Disable Cargo's *global* credential providers so that Cargo does not attempt to look up registry tokens + # on its own. The dependabot proxy (https://github.com/dependabot/proxy/) handles all registry authentication + # transparently by intercepting HTTP requests and injecting the appropriate credentials. + # + # Note: this only affects the global/default credential provider. Per-registry `credential-provider` settings + # in .cargo/config.toml override this env var, so those are stripped separately by `sanitize_cargo_config`. + # + # Uses ||= so developers can override by setting CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=cargo:token in their + # shell (along with the appropriate CARGO_REGISTRIES_{NAME}_TOKEN vars) for local development without the proxy. sig { void } def self.bypass_cargo_credential_providers - # Disable Cargo's built-in credential providers entirely so that Cargo does not attempt to look up registry - # tokens on its own. The dependabot proxy (https://github.com/dependabot/proxy/) handles all registry - # authentication transparently by intercepting HTTP requests and injecting the appropriate credentials. - # - # Uses ||= so developers can override by setting CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=cargo:token in their - # shell (along with the appropriate CARGO_REGISTRIES_{NAME}_TOKEN vars) for local development without the proxy. ENV["CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS"] ||= "" end + + # Strip per-registry `credential-provider` settings from .cargo/config.toml. + # + # Users may have entries like: + # [registries.my-registry] + # credential-provider = "cargo:token" + # + # These per-registry settings override the global CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS env var, + # causing Cargo to look up tokens locally. Since the dependabot proxy handles all registry authentication + # transparently, we remove these so Cargo makes plain unauthenticated requests that the proxy can intercept. + sig { params(config_content: String).returns(String) } + def self.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(config_content) + return config_content unless parsed.is_a?(Hash) + + registries = parsed["registries"] + if registries.is_a?(Hash) + registries.each_value do |registry_config| + registry_config.delete("credential-provider") if registry_config.is_a?(Hash) + end + end + + # Also strip credential-provider from [registry] (crates.io default registry). Users who `cargo publish` + # from CI may have this set. It's a per-registry override that takes precedence over the global env var, + # so we need to remove it to prevent Cargo from trying to look up a token. + registry = parsed["registry"] + registry.delete("credential-provider") if registry.is_a?(Hash) + + TomlRB.dump(parsed) + rescue TomlRB::Error => e + raise Dependabot::DependencyFileNotParseable.new( + ".cargo/config.toml", + "Failed to parse Cargo config file: #{e.message}" + ) + end end end end diff --git a/cargo/lib/dependabot/cargo/update_checker/version_resolver.rb b/cargo/lib/dependabot/cargo/update_checker/version_resolver.rb index f30b813fe9..9c6e0aff01 100644 --- a/cargo/lib/dependabot/cargo/update_checker/version_resolver.rb +++ b/cargo/lib/dependabot/cargo/update_checker/version_resolver.rb @@ -215,10 +215,11 @@ def write_temporary_dependency_files(prepared: true) File.write(T.must(lockfile).name, T.must(lockfile).content) if lockfile File.write(T.must(toolchain).name, T.must(toolchain).content) if toolchain - return unless config + config_file = config + return unless config_file - FileUtils.mkdir_p(File.dirname(T.must(config).name)) - File.write(T.must(config).name, T.must(config).content) + FileUtils.mkdir_p(File.dirname(config_file.name)) + File.write(config_file.name, Helpers.sanitize_cargo_config(T.must(config_file.content))) end sig { void } diff --git a/cargo/spec/dependabot/cargo/helpers_spec.rb b/cargo/spec/dependabot/cargo/helpers_spec.rb index 00fbf10d8f..18060ac89a 100644 --- a/cargo/spec/dependabot/cargo/helpers_spec.rb +++ b/cargo/spec/dependabot/cargo/helpers_spec.rb @@ -34,4 +34,129 @@ end end end + + describe ".sanitize_cargo_config" do + context "when config has no credential-provider settings" do + let(:config_content) do + <<~TOML + [registries.my-registry] + index = "sparse+https://example.com/index/" + token = "some-token" + TOML + end + + it "returns equivalent config with non-credential-provider keys preserved" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + expect(parsed["registries"]["my-registry"]["index"]).to eq("sparse+https://example.com/index/") + expect(parsed["registries"]["my-registry"]["token"]).to eq("some-token") + end + end + + context "when config has no registries section" do + let(:config_content) do + <<~TOML + [source.crates-io] + replace-with = "my-mirror" + + [net] + git-fetch-with-cli = true + TOML + end + + it "returns equivalent config unchanged" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + expect(parsed["source"]["crates-io"]["replace-with"]).to eq("my-mirror") + expect(parsed["net"]["git-fetch-with-cli"]).to be true + end + end + + context "when config has per-registry credential-provider" do + let(:config_content) do + <<~TOML + [registries.artifactory] + index = "sparse+https://example.com/api/cargo/cargo-local/index/" + credential-provider = "cargo:token" + + [registries.artifactory-remote] + index = "sparse+https://example.com/api/cargo/cargo-crates-remote/index/" + credential-provider = "cargo:token" + TOML + end + + it "strips credential-provider from all registries" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + + expect(parsed["registries"]["artifactory"]).not_to have_key("credential-provider") + expect(parsed["registries"]["artifactory-remote"]).not_to have_key("credential-provider") + end + + it "preserves index URLs" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + + expect(parsed["registries"]["artifactory"]["index"]) + .to eq("sparse+https://example.com/api/cargo/cargo-local/index/") + expect(parsed["registries"]["artifactory-remote"]["index"]) + .to eq("sparse+https://example.com/api/cargo/cargo-crates-remote/index/") + end + end + + context "when config has [registry] credential-provider (e.g. for cargo publish)" do + let(:config_content) do + <<~TOML + [registry] + credential-provider = "cargo:token" + TOML + end + + it "strips the credential-provider" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + + expect(parsed.fetch("registry", {})).not_to have_key("credential-provider") + end + end + + context "when config has mixed settings" do + let(:config_content) do + <<~TOML + [registries.with-cred] + index = "sparse+https://example.com/index/" + credential-provider = "cargo:token" + + [registries.without-cred] + index = "sparse+https://other.example.com/index/" + + [source.crates-io] + replace-with = "with-cred" + + [net] + git-fetch-with-cli = true + TOML + end + + it "strips only credential-provider, preserves everything else" do + result = described_class.sanitize_cargo_config(config_content) + parsed = TomlRB.parse(result) + + expect(parsed["registries"]["with-cred"]).not_to have_key("credential-provider") + expect(parsed["registries"]["with-cred"]["index"]).to eq("sparse+https://example.com/index/") + expect(parsed["registries"]["without-cred"]["index"]).to eq("sparse+https://other.example.com/index/") + expect(parsed["source"]["crates-io"]["replace-with"]).to eq("with-cred") + expect(parsed["net"]["git-fetch-with-cli"]).to be true + end + end + + context "when config is malformed TOML" do + let(:config_content) { "this is not valid toml {{{{" } + + it "raises DependencyFileNotParseable" do + expect { described_class.sanitize_cargo_config(config_content) } + .to raise_error(Dependabot::DependencyFileNotParseable, /Failed to parse Cargo config file/) + end + end + end end