Centralise outbound URL validation in StrictHTTPClient - #4246
Conversation
Move core.ParsePublicURL from per-caller into StrictHTTPClient.Do so every outbound HTTP request gets HTTPS-only + no-IP + no-RFC2606-reserved-host validation in strict mode. Add CheckRedirect to re-validate every redirect target (10-hop cap matches net/http's default). Removes the duplicated validation in auth/openid4vci.Client, auth/client/iam.HTTPClient.ClientMetadata, and the scheme-only check in auth/services/oauth/relying_party.RequestRFC003AccessToken. All production callers route through *StrictHTTPClient. Behavior change: strict mode now rejects IP literals and RFC 2606 reserved hostnames on every outbound HTTP request, not just OpenID4VCI. Breaking API: oauth.NewRelyingParty no longer accepts a strictMode bool parameter; strict-mode behaviour is now controlled exclusively by the http/client.StrictMode flag set by the HTTP engine at startup. Closes #4244
There was a problem hiding this comment.
Pull request overview
Centralizes SSRF-oriented outbound URL validation inside http/client.StrictHTTPClient so all outbound HTTP calls (and redirect targets) are consistently validated via core.ParsePublicURL, and removes duplicated per-caller checks across auth-related clients.
Changes:
- Validate every outbound request URL in
StrictHTTPClient.Doviacore.ParsePublicURL, gated by the globalhttp/client.StrictMode. - Enforce URL validation on redirects by wiring
CheckRedirect(with a 10-hop cap) into allStrictHTTPClientconstructors. - Remove per-caller strict-mode URL checks and adjust APIs/tests/docs accordingly (incl. dropping
strictModefromoauth.NewRelyingPartyandopenid4vci.NewClient).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
http/client/client.go |
Adds centralized ParsePublicURL validation and redirect re-validation via CheckRedirect. |
http/client/client_test.go |
Updates strict-mode error expectations and adds coverage for IP/reserved-host blocking + redirects. |
auth/openid4vci/client.go |
Removes per-client URL validation and drops strictMode parameter from NewClient. |
auth/openid4vci/client_test.go |
Updates construction of the OpenID4VCI client to match the new API. |
auth/client/iam/client.go |
Removes per-method URL validation for client metadata retrieval (now inherited from shared HTTP client). |
auth/services/oauth/relying_party.go |
Removes relying-party strictMode parameter/field; relies on shared HTTP client strict mode. |
auth/services/oauth/relying_party_test.go |
Removes tests that exercised relying-party-local scheme validation (now centralized). |
auth/auth.go |
Updates call sites for the changed openid4vci.NewClient and oauth.NewRelyingParty signatures. |
vcr/vcr_test.go |
Updates strict-mode error assertions to match new centralized validation errors. |
docs/pages/deployment/configuration.rst |
Documents stricter outbound URL validation (IP literals + RFC2606 reserved hosts) and redirect enforcement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Apply the capture-and-restore t.Cleanup pattern to tests that mutate the package-level StrictMode and DefaultCachingTransport globals so they no longer leak state into subsequent tests. Adds a withClientGlobals helper in http/client/client_test.go to avoid repeating the boilerplate. Also fixes a pre-existing DefaultCachingTransport leak in TestCaching.
|
We need to check: I think CRLs are sometimes published on plain HTTP endpoints (instead of HTTPS). @JorisHeadease could you check that? |
…alise-url-validation ; Conflicts: ; auth/openid4vci/client.go ; auth/openid4vci/client_test.go ; vcr/vcr_test.go
StrictHTTPClient.Do and its CheckRedirect now run core.ParsePublicURLAllowIP (HTTPS-only + RFC 2606 reserved-hostname rejection in strict mode) on every outbound request and every redirect hop, instead of each caller duplicating that check. Removes the duplicated validation from auth/openid4vci.Client, auth/client/iam.HTTPClient.ClientMetadata, auth/client/iam.OpenID4VPClient, auth/oauth.FetchMetadata/IssuerIdToWellKnown, and auth/services/oauth.relyingParty, dropping the strictMode parameter each of them threaded through solely for that purpose. IP-literal hosts are deliberately left unchecked by this centralised validation: http/client's dial-time SSRF guard (added in #4420) already polices connecting addresses and honors http.client.allowedinternalcidrs/ deniedcidrs, which a blanket string-level IP check has no visibility into and would make unreachable for IP-literal targets. core.ParsePublicURLAllowIP is a new variant of ParsePublicURL that keeps the HTTPS/reserved-hostname checks but exempts IP literals for this reason. auth.Auth.strictMode was a dead field (never assigned, always false), so the openid4vci and IAM client-metadata paths that read it were effectively never strict-mode-validated at the URL layer regardless of the real strictmode setting. Removing it in favour of the correctly-wired http/client.StrictMode global fixes that as a side effect. Closes #4244.
|
CRL fetching ( |
4 new issues
|
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (9)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|

Closes #4244.
What changed
http/client.StrictHTTPClient.Doand itsCheckRedirectpolicy now validate every outbound URL and every redirect target viacore.ParsePublicURLAllowIP(HTTPS-only + no RFC 2606 reserved hostnames in strict mode), instead of each caller duplicating that check.Per-caller URL validation removed from:
auth/openid4vci.Clientauth/client/iam.HTTPClient.ClientMetadataauth/client/iam.OpenID4VPClient(6 call sites:PostError,PostAuthorizationResponse,PresentationDefinition,RequestObjectByGet,RequestObjectByPost,AccessToken)auth/oauth.FetchMetadata/wellKnownCandidates/IssuerIdToWellKnownauth/services/oauth.relyingParty.RequestRFC003AccessToken(scheme-only check)All of these routed through
*StrictHTTPClientalready, so the checks were purely duplicated.IP-literal hosts are deliberately exempt from this check
core.ParsePublicURL's "hostname is IP" rejection is not part of the centralized check. Blocking IP-literal URLs at this layer would makehttp.client.allowedinternalcidrs(#4420) unreachable for IP-literal targets — that config option exists precisely to let internal flows target a private address, and the guard that honors it is the dial-time SSRF guard (denyNonPublicAddr), not a URL-string check. So IP addresses are left entirely to that guard, which already runs on every connection (including redirect hops) and is allowlist/denylist-aware.A new
core.ParsePublicURLAllowIPbehaves likecore.ParsePublicURLexcept it never rejects an IP-literal host, for this reason.Breaking API change
oauth.NewRelyingParty,openid4vci.NewClient,oauth.FetchMetadata,oauth.IssuerIdToWellKnownno longer accept astrictMode/strictmodeparameter.iam.ClientConfig.StrictModeandiam.OpenID4VPClient's equivalent field are removed. Strict-mode behavior is controlled exclusively byhttp/client.StrictMode, set at startup fromcore.ServerConfig.Strictmode.Side effect: fixes a live gap
auth.Auth.strictModewas a dead field — declared, threaded intoiam.ClientConfig.StrictModeandopenid4vci.NewClient, but never assigned, so it was alwaysfalse. That meant theopenid4vciand IAM client-metadata URL-validation paths never actually enforced strict-mode SSRF rules regardless of the realstrictmodeconfig value. Removing it in favor of the correctly-wiredhttp/client.StrictModeglobal fixes that.Out of scope
DNS rebinding and IP-literal SSRF are handled by #4420's dial guard, already merged. This PR only removes the now-redundant string-level validation duplicated across callers.