Skip to content

Reject ambiguous NuGet credential routes - #206

Closed
jeffwidman wants to merge 1 commit into
defer-nuget-service-index-discoveryfrom
reject-ambiguous-nuget-resource-routes
Closed

Reject ambiguous NuGet credential routes#206
jeffwidman wants to merge 1 commit into
defer-nuget-service-index-discoveryfrom
reject-ambiguous-nuget-resource-routes

Conversation

@jeffwidman

Copy link
Copy Markdown
Member

Summary

This is a security-policy discussion draft stacked on #203. It changes how the proxy handles the same normalized NuGet route being claimed by multiple configured service indexes.

Today, the first service-index response to register a discovered route supplies its credential. When different indexes claim the same route with different credentials, response timing can therefore decide which credential is injected. Deterministic selection would remove timing variance but would not establish that the selected credential is authorized for the shared route.

Proposed policy

  • The same route claimed with the same effective credential is deduplicated and remains authenticated.
  • Static credentials are compared by their effective authorization value, so token: "user:password" is equivalent to matching username/password fields.
  • OIDC credentials are equivalent when their provider parameters are equal; cached token state is ignored.
  • The same route claimed with different static credentials, different OIDC credentials, or mixed static/OIDC credentials is marked conflicting.
  • Requests matching a conflicting route continue upstream, but the NuGet handler injects no OIDC, URL, or host-fallback credential. Public shared endpoints therefore continue to work anonymously; private endpoints fail visibly with their normal authentication response.
  • The conflict and all claiming service-index URLs are logged without credential material.

What this does not reject

Two feeds or mirrors may offer the same package ID/version at different resource URLs. Those routes remain independent and each receives its own credential. The proxy does not attempt to compare package contents or decide NuGet source precedence.

For example, this remains valid:

Feed A -> https://feed-a.example/packages/
Mirror -> https://mirror.example/packages/

Only an exact normalized route claim is considered ambiguous:

Feed A ----> https://shared.example/packages/
Feed B ----> https://shared.example/packages/

Implementation notes

A claim table is seeded for configured service-index URLs and extended for authenticated redirects and discovered resource URLs. The conflict check runs before every NuGet OIDC, static URL, and host-only lookup. The first credential may remain stored in an underlying registry, but it is unreachable for a conflicted route while the deny guard is active.

The current draft conservatively blocks NuGet credential injection for descendants of a conflicting route, even if a separately configured route is more specific. That favors fail-closed behavior but is an explicit point for review.

Questions for review

  1. Is proceeding anonymously and allowing the upstream endpoint to return 401/403 preferable to failing the request directly in the proxy?
  2. Should a more-specific non-conflicting route be allowed beneath a conflicting parent route, or should any matching conflict remain fail-closed?
  3. Is equality of effective static auth and OIDC provider parameters the right definition of “same credential”?
  4. Should conflict state remain an in-memory guard, or should conflicting entries also be removed from the underlying static/OIDC registries?
  5. Do we know of legitimate private NuGet services where different credentials intentionally share exactly the same resource URL?

Validation

  • Same effective static credentials can share a route, including equivalent token and username/password representations.
  • Equivalent OIDC provider parameters do not conflict.
  • Different static, different OIDC, and mixed static/OIDC claims fail closed.
  • Both response orders produce the same result.
  • Mirrors with distinct URLs remain independently authenticated.
  • go test -race ./internal/handlers ./internal/oidc -count=1 passes.

Stack

@brettfo

brettfo commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

I don't recommend rejecting requests to the ambiguous URLs as I think there is a possible case for the URL collision to happen naturally, but it's not actually a security issue.

Short version: when a duplicate NuGet URL is detected, log that it is being ignored because it was already registered, but do not prevent requests to it.

High level overview of NuGet

This high level overview of NuGet is overly simplified and in some cases wrong, but it's enough to understand the issues here.

A NuGet feed is specified by a URL like .../nuget/v3/index.json. That index file lists several other URLs that the NuGet client will use to construct other URLs. One such URL is PackageBaseAddress which is used to download the actual package. The NuGet client will concat a string like /{packageName}/{packageVersion}/{packageName}.{packageVersion}.nupkg to the PackageBaseAddress to download the package.

Accidental conflict

It's plausible for two different NuGet feeds to report the same PackageBaseAddress but it shouldn't be an issue. For this example I'm going to use Azure DevOps Artifacts feeds. The example here isn't 100% correct but the idea is the same.

An Azure DevOps feed can look like this: https://pkgs.dev.azure.com/ORGANIZATION/PROJECT/_packaging/FEED_NAME/nuget/v3/index.json and that will list a PackageBaseAddress. However, there is an older style of feed URL that should be exactly the same at https://pkgs.ORGANIZATION.visualstudio.com/PROJECT/_packaging/FEED_NAME/nuget/v3/index.json and that could list the exact same PackageBaseAddress. Consider a repo with the first URL style (dev.azure.com) and an associated secret injected at runtime. Then consider a developer on that team adds the second URL style (visualstudio.com) to the NuGet config file along with its own associated secret. The end result is that the proxy will try to register both identical PackageBaseAddress URLs with different credentials. If this potentially ambiguous URL is rejected outright then dependabot jobs will fail because those URLs won't be authenticated. If the ambiguous URLs are allowed but the credentials are swapped, then the same endpoint will get a different secret, but it's still one that's allowed and still controlled by the same owner.

Given that the initial /index.json endpoint controls the rest of the URLs, the only way to inject a compromised URL would be to compromise the /index.json endpoint itself and at that point, you're already compromised.

Engineered conflict

  • A repo uses a protected package feed: https://private.example.com/nuget/v3/index.json.
  • That feed specifies a PackageBaseAddress of https://private.example.com/nuget/v3/package/.
  • The credentials associated with this feed will be sent in both of these URLs.
  • A malicious actor convinces a developer on that team to add a new package feed of: https://malicious.example.com/nuget/v3/index.json.
  • That malicious feed specifies a PackageBaseAddress of https://private.example.com/nuget/v3/package/ as well.
  • A duplicate PackageBaseAddress is detected, so how is it handled:
    1. If we decide to reject requests to the ambiguous URL, then a legitimate request to download the package will fail and their update job will not complete. Given that this was a malicious feed, maybe that's OK, but consider the accidental conflict above. Conclusion: don't block ambiguous URLs.
    2. If we only log the duplicate ambiguous URL and keep the initial credentials intact, then any package that the malicious feed claims to have will just redirect to the real internal feed and use the appropriate credentials. This is not a problem because the credentials were never sent to the malicious actor. Conclusion: logging the ambiguous URL but keeping the original credentials is an acceptable approach.
    3. If we instead replace/overwrite the duplicate URL credentials with the new ones, e.g., we replace VALID_INTERNAL_TOKEN with MALICIOUS_TOKEN, then even a legitimate request to download the package will fail because the credentials are incorrect, so the only negative behavior is like above: an update job won't complete but nothing was compromised. Conclusion: replacing the duplicate URL credentials is an acceptable approach.
    4. Note that if the malicous feed is processed first then the behavior of items 2 and 3 above will be reversed, and either is acceptable and not a security issue.

@brettfo brettfo closed this Aug 17, 2026
@jeffwidman
jeffwidman deleted the reject-ambiguous-nuget-resource-routes branch August 18, 2026 00:07
@jeffwidman

Copy link
Copy Markdown
Member Author

I added the suggested log line about ignoring duplicate URLs to:

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