Skip to content

Cross-ecosystem credential clobbering #232

Description

@copiousfreetime

Problem

When two or more private registries for different package ecosystems live on the same hostname, the Dependabot proxy applies all of their credentials to the same outbound request, each overwriting the last — so the credential that actually reaches the registry is whichever ecosystem's handler runs last in the proxy's fixed handler chain, not the one for the ecosystem being resolved.

We encountered this since all of our repositories were in cloudsmith, and have a common prefix, and we had one that the url was truncated when configured.

How?

1. Credentials are matched per-handler, by host + naive path prefix

return strings.HasPrefix(req.URL.Path, strings.TrimRight(parsedURL.Path, "/"))

A registry configured as https://host/base matches every request under /base/..., including paths belonging to entirely different ecosystems. Virtual every registry handler uses this same logic. e.g.

if !helpers.CredentialURLMatchesRequest(req, cred.url, true) &&

if !helpers.UrlMatchesRequest(req, cred.url, true) && !helpers.CheckHost(req, cred.host) {

2. Every handler runs on every request

The handlers are registered as an unconditional OnRequest chain in proxy.go. There is no "this request belongs to ecosystem X, skip the others" dispatch.

proxy/proxy.go

Lines 106 to 150 in 49c2109

npmRegistryHandler := handlers.NewNPMRegistryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(npmRegistryHandler.HandleRequest)
hexOrganizationHandler := handlers.NewHexOrganizationHandler(cfg.Credentials)
proxy.OnRequest().DoFunc(hexOrganizationHandler.HandleRequest)
hexRepositoryHandler := handlers.NewHexRepositoryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(hexRepositoryHandler.HandleRequest)
pythonHandler := handlers.NewPythonIndexHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(pythonHandler.HandleRequest)
proxy.OnResponse().DoFunc(pythonHandler.HandleResponse)
composerHandler := handlers.NewComposerHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(composerHandler.HandleRequest)
dockerRegistryHandler := handlers.NewDockerRegistryHandler(cfg.Credentials, oidcClient, nil)
proxy.OnRequest().DoFunc(dockerRegistryHandler.HandleRequest)
rubyGemsServerHandler := handlers.NewRubyGemsServerHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(rubyGemsServerHandler.HandleRequest)
proxy.OnRequest().DoFunc(nugetFeedHandler.HandleRequest)
proxy.OnResponse().DoFunc(nugetFeedHandler.HandleResponse)
mavenRepositoryHandler := handlers.NewMavenRepositoryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(mavenRepositoryHandler.HandleRequest)
terraformRegistryHandler := handlers.NewTerraformRegistryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(terraformRegistryHandler.HandleRequest)
openTofuRegistryHandler := handlers.NewOpenTofuRegistryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(openTofuRegistryHandler.HandleRequest)
pubRepositoryHandler := handlers.NewPubRepositoryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(pubRepositoryHandler.HandleRequest)
cargoRegistryHandler := handlers.NewCargoRegistryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(cargoRegistryHandler.HandleRequest)
goProxyServerHandler := handlers.NewGoProxyServerHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(goProxyServerHandler.HandleRequest)
helmRegistryHandler := handlers.NewHelmRegistryHandler(cfg.Credentials, oidcClient)
proxy.OnRequest().DoFunc(helmRegistryHandler.HandleRequest)

3. Applying a credential replaces the header

In each handler, if it matches (1) then the last handler that matches the URL will set the authorization headers.

4. Order is fixed and undocumented

Last matching handler wins. From proxy.go, the credential-injecting registry
handlers in execution order:

# Handler Note
1 npm_registry most easily clobbered
2 hex_organization
3 hex_repository
4 python_index
5 composer
6 docker_registry
7 rubygems_server
8 nuget_feed
9 maven_repository
10 terraform_registry
11 opentofu_registry
12 pub_repository
13 cargo_registry
14 goproxy_server
15 helm_registry always wins

So a Go credential silently overrides a Python one; a Helm credential overrides everything.

Replication

All of the following must happen for this to show up

  1. Two or more private registries resolving to the same hostname (via repo dependabot.yml registries: or org-level private registry config).
  2. At least one configured URL is a path prefix of another's request paths — classically a bare base URL like https://host/base with no ecosystem or repo segment. A bare host field does this unconditionally.
  3. The handler that runs later holds credentials the target registry rejects — different account, different token type, or an empty password.
  4. The overridden ecosystem depends on the proxy for auth. For Python this is guaranteed when using username/password, because AuthedUrlBuilder only embeds credentials into the index URL when a token field is present:

If all same-host registries happen to share one credential, the clobber still happens, but it doesn't effect the process since its a valid auth for all of the registries.

Signature to look for in a job log

[016] GET https://HOST/base/org/py-repo/python/simple/somepkg/
[016] * authenticating python index request        (host: HOST)   <- intended
[016] * authenticating nuget feed request          (host: HOST)
[016] * authenticating maven repository request    (host: HOST)
[016] * authenticating terraform registry request  (host: HOST)
[016] * authenticating goproxy request             (host: HOST)   <- this one wins
[016] 401 https://HOST/base/org/py-repo/python/simple/somepkg/

More than one * authenticating line per request ID means credentials are colliding.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions