fix(distribution): validate token realm on pull and re-challenge paths - #1038
Merged
Conversation
CVE-2026-33990 was fixed only in the hand-rolled Exchange() used by the push flow. The pull path (remote.Image -> createResolver) and the push re-challenge authorizer build containerd's default authorizer, which follows the realm URL from a 401 WWW-Authenticate challenge without validating it, so a malicious registry can drive a token fetch at an internal address and turn Model Runner into an SSRF proxy. Guard the HTTP client containerd uses for token fetches via docker.WithAuthClient. Its dialer validates the resolved IP against the private/loopback/link-local blocklist and dials that exact address, covering the pull, push, and fallback resolvers uniformly instead of only the hand-rolled Exchange().
Drive the pull from POST /models/create through the manager, distribution client, and containerd resolver against a malicious registry that advertises a loopback token realm. Asserts the registry is contacted but the realm is never followed, so the internal service receives nothing. Fails without the WithAuthClient guard.
ilopezluna
marked this pull request as ready for review
August 11, 2026 13:31
Contributor
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- newGuardedAuthClient only preserves settings when the base is an http.Transport and silently falls back to cloning http.DefaultTransport for other RoundTrippers, which can drop custom behavior (e.g., proxies, timeouts, middleware); consider either explicitly documenting this or handling non-Transport bases by wrapping them instead of replacing them.
- resolveAndValidateHost is now a generic validator used by the guarded auth client, but the error messages are still realm-specific (e.g., "realm URL"), which may be confusing when surfaced from other callers; consider rewording or adding caller-specific context when returning these errors.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- newGuardedAuthClient only preserves settings when the base is an *http.Transport* and silently falls back to cloning http.DefaultTransport for other RoundTrippers, which can drop custom behavior (e.g., proxies, timeouts, middleware); consider either explicitly documenting this or handling non-Transport bases by wrapping them instead of replacing them.
- resolveAndValidateHost is now a generic validator used by the guarded auth client, but the error messages are still realm-specific (e.g., "realm URL"), which may be confusing when surfaced from other callers; consider rewording or adding caller-specific context when returning these errors.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
ericcurtin
approved these changes
Aug 11, 2026
doringeman
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
resolveAndValidateRealm(the SSRF guard added for CVE-2026-33990 / GHSA-x2f5-332j-9xwq) is only reached from the hand-rolledExchange(), which is used exclusively by the push flow. Two authorizers are built with containerd's defaultdocker.NewDockerAuthorizerand never see that guard:remote.Image()→createResolver();createResolverWithPushScope()(also reused by every fallback in that function).containerd follows the
realmURL from a401 WWW-Authenticate: Bearerchallenge with its own auth client and no validation, so a malicious registry can point the realm at an internal address (127.0.0.1,169.254.169.254, RFC-1918, …) and make Model Runner fetch it — SSRF, and the same class of bug the original CVE fixed only forExchange().How
Guard the HTTP client containerd uses for token fetches only. In this containerd version the authorizer's client (set via
docker.WithAuthClient) is used solely byauth.FetchToken/FetchTokenWithOAuthto contact the realm, so a dialer-level check there covers every authorizer-building path without touching the data-plane connection to the registry the user explicitly targeted (local/insecure registries keep working).newGuardedAuthClientbuilds that client; itsDialContextvalidates the resolved IP against the existing private/loopback/link-local blocklist and dials that exact address, so DNS rebinding can't slip an internal IP past the check (TOCTOU-safe).createResolverand thecreateResolverWithPushScopere-challenge authorizer viadocker.WithAuthClient.resolveAndValidateHostfromresolveAndValidateRealmso the realm check and the new dialer share one blocklist — no duplicated CIDRs.Tests
TestCreateModelSSRF_RealmNotFollowedToInternalService— endpoint-level end to end: drivesPOST /models/createthrough the manager, distribution client, and containerd resolver against a malicious registry that advertises a loopback token realm. Asserts the registry is contacted (the pull path really runs) but the realm is never followed, so the internal service receives nothing. Verified to fail without the guard.TestPullSSRF_RealmNotFollowedToInternalService— same scenario one layer down atremote.Image, the exact path the original fix missed.TestNewGuardedAuthClientBlocksLoopbackandTestResolveAndValidateHost— unit coverage for the guarded client and the shared validator.ExchangeSSRF tests still pass.Notes
rangeTransportfollows blob-download redirects on the data-plane without validating the destination. Happy to follow up if we want to close that too.