Skip to content

Commit a2f47d4

Browse files
authored
transport: validate Bearer realm URL to prevent SSRF (#2243)
fromChallenge() stored the realm value from a WWW-Authenticate header verbatim without validation. A malicious or MITM'd registry could supply a realm pointing at a private/link-local address (e.g. 169.254.169.254) or use a non-HTTP scheme, causing the client to make token-fetch requests to internal services when pulling images. Add validateRealmURL() which enforces: - Scheme allowlist: only https is accepted for secure registries; http is additionally accepted when the registry is marked insecure. - IP literal blocklist: loopback, link-local unicast/multicast, and private-range addresses are rejected. This blocks direct SSRF to cloud instance metadata services and RFC 1918 networks. DNS-based SSRF is out of scope and should be handled at the network layer.
1 parent 19a36cd commit a2f47d4

1 file changed

Lines changed: 39 additions & 0 deletions

File tree

pkg/v1/remote/transport/bearer.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTrip
8282
if !ok {
8383
return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters)
8484
}
85+
// Validate the realm URL before storing it. A malicious or compromised
86+
// registry can supply a realm pointing at an internal service or cloud
87+
// metadata endpoint (e.g. 169.254.169.254), causing SSRF when the client
88+
// subsequently fetches a token.
89+
if err := validateRealmURL(realm, pr.Insecure); err != nil {
90+
return nil, fmt.Errorf("invalid realm in www-authenticate: %w", err)
91+
}
8592
service := pr.Parameters["service"]
8693
scheme := "https"
8794
if pr.Insecure {
@@ -98,6 +105,38 @@ func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTrip
98105
}, nil
99106
}
100107

108+
// validateRealmURL returns an error if the realm URL uses a disallowed scheme
109+
// or resolves to a private / link-local IP address. This prevents a crafted
110+
// WWW-Authenticate header from redirecting token fetches to internal services.
111+
func validateRealmURL(realm string, insecure bool) error {
112+
u, err := url.Parse(realm)
113+
if err != nil {
114+
return fmt.Errorf("parsing realm %q: %w", realm, err)
115+
}
116+
switch u.Scheme {
117+
case "https":
118+
// always allowed
119+
case "http":
120+
if !insecure {
121+
return fmt.Errorf("realm scheme %q not allowed for a secure registry; use https", u.Scheme)
122+
}
123+
default:
124+
return fmt.Errorf("realm scheme %q not allowed; must be https (or http for insecure registries)", u.Scheme)
125+
}
126+
// Reject IP literals that resolve to private or link-local ranges.
127+
// This blocks direct references to RFC 1918 addresses, loopback, and
128+
// link-local ranges including the cloud instance metadata service
129+
// (169.254.169.254 / fd00:ec2::254). DNS-based SSRF is out of scope
130+
// here; callers should apply network-level controls if needed.
131+
host := u.Hostname()
132+
if ip := net.ParseIP(host); ip != nil {
133+
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() {
134+
return fmt.Errorf("realm host %q is a private or link-local address", host)
135+
}
136+
}
137+
return nil
138+
}
139+
101140
type bearerTransport struct {
102141
mx sync.RWMutex
103142
// Wrapped by bearerTransport.

0 commit comments

Comments
 (0)