Summary
The upstream-DCR resolver dials the discovery URL and the registration
endpoint with an HTTP client that has no SSRF protection, and it gives the
caller no way to supply a guarded client. A caller cannot restrict these
outbound calls to non-private hosts, so a DCR upstream is an SSRF vector
(CWE-918) even when the caller validates the configured URL up front.
Where
pkg/auth/dcr/resolver.go builds its HTTP client internally:
httpClient := newDCRHTTPClient(req.InitialAccessToken)
newDCRHTTPClient wraps oauthproto.NewDefaultDCRClient(), which is a plain
client with timeouts and redirect refusal only — no dialer control, no
private-IP guard:
// pkg/oauthproto/dcr.go
func NewDefaultDCRClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
},
}
}
ResolveCredentials(ctx, req, cache) takes no *http.Client, and dcr.Request
has no AllowPrivateIPs field, so the caller cannot inject a guarded client or
opt into a private-IP policy.
Why it matters
The OAuth2 and OIDC upstream configs already carry an AllowPrivateIPs flag and
dial through a private-IP-guarded client (safe-by-default). The DCR resolver is
the one upstream-facing path with no equivalent. Two concrete vectors:
- Discovery indirection. A
DiscoveryURL on an allowed host resolves (RFC
8414) to a registration_endpoint the metadata document controls. That
resolved endpoint is dialed without any host check — it can point at a
link-local metadata address or an in-cluster service.
- DNS rebinding. A directly-configured
RegistrationEndpoint that a caller
validated at configure time can resolve to a private address at dial time.
Expected
Mirror the OAuth2/OIDC upstream posture. Either:
- add an
AllowPrivateIPs field to dcr.Request and dial through a private-IP-
guarded client (safe-by-default), or
- let
ResolveCredentials accept a caller-supplied *http.Client so the caller
can enforce its own dial policy on both the discovery fetch and the
registration POST.
Version
Observed in v0.34.0. Please confirm on main.
Note
Downstream (Airlock) is proceeding with write-time URL validation as a
best-effort guard and documenting the residual (discovery indirection + DNS
rebinding) as a known risk, mitigated at deployment by a gateway egress
NetworkPolicy. A guarded/injectable DCR client would let us close it properly.
Related: toolhive#5823 (the DCR cache key omits the upstream identity).
Summary
The upstream-DCR resolver dials the discovery URL and the registration
endpoint with an HTTP client that has no SSRF protection, and it gives the
caller no way to supply a guarded client. A caller cannot restrict these
outbound calls to non-private hosts, so a DCR upstream is an SSRF vector
(CWE-918) even when the caller validates the configured URL up front.
Where
pkg/auth/dcr/resolver.gobuilds its HTTP client internally:newDCRHTTPClientwrapsoauthproto.NewDefaultDCRClient(), which is a plainclient with timeouts and redirect refusal only — no dialer control, no
private-IP guard:
ResolveCredentials(ctx, req, cache)takes no*http.Client, anddcr.Requesthas no
AllowPrivateIPsfield, so the caller cannot inject a guarded client oropt into a private-IP policy.
Why it matters
The OAuth2 and OIDC upstream configs already carry an
AllowPrivateIPsflag anddial through a private-IP-guarded client (safe-by-default). The DCR resolver is
the one upstream-facing path with no equivalent. Two concrete vectors:
DiscoveryURLon an allowed host resolves (RFC8414) to a
registration_endpointthe metadata document controls. Thatresolved endpoint is dialed without any host check — it can point at a
link-local metadata address or an in-cluster service.
RegistrationEndpointthat a callervalidated at configure time can resolve to a private address at dial time.
Expected
Mirror the OAuth2/OIDC upstream posture. Either:
AllowPrivateIPsfield todcr.Requestand dial through a private-IP-guarded client (safe-by-default), or
ResolveCredentialsaccept a caller-supplied*http.Clientso the callercan enforce its own dial policy on both the discovery fetch and the
registration POST.
Version
Observed in
v0.34.0. Please confirm onmain.Note
Downstream (Airlock) is proceeding with write-time URL validation as a
best-effort guard and documenting the residual (discovery indirection + DNS
rebinding) as a known risk, mitigated at deployment by a gateway egress
NetworkPolicy. A guarded/injectable DCR client would let us close it properly.
Related: toolhive#5823 (the DCR cache key omits the upstream identity).