Go 1.23.5 preserves credential removal across redirects
January's security release repairs HTTP redirect credential handling and URI certificate constraints. Both fixes depend on preserving the meaning of a trust boundary across parsing and repeated operations.
An HTTP client can make the right decision on one redirect and still leak credentials on the next. Go's January security release fixes that exact failure. A request carrying an Authorization header could leave its original domain without forwarding the header, then send it to the new domain after another redirect. The first transition appeared safe when examined alone. The complete sequence was unsafe.
Go 1.23.5, released on January 16, includes this correction alongside a certificate-verification fix and ordinary maintenance changes. The security patches also ship in Go 1.22.11. The release announcement identifies the redirect problem as CVE-2024-45336 and the certificate problem as CVE-2024-45341. Their mechanisms are different, but both concern the interpretation of identity by standard-library code.
I value Go's readable control flow because it makes operational decisions inspectable. These bugs show why that preference must extend into library implementations. A caller can write a straightforward request and still depend on a state transition inside the client that is difficult to see at the call site.
Why the second redirect matters
The announcement gives a concrete sequence. A client requests a.com/ with an Authorization header. That server redirects to b.com/1, and the client removes the sensitive header. The second server then redirects to b.com/2. In the affected implementation, that same-domain transition could restore the original Authorization header. The original credential reached a destination outside its intended domain.
The important distinction is between the relationship of two adjacent destinations and the history of the request. Once a chain has crossed the boundary that requires credential removal, a later local transition does not grant permission to recover the original credentials. Comparing only the newest pair of URLs cannot express that rule. The client must remember that it already removed sensitive headers.
The Go 1.23.5 client implementation makes that memory explicit through stripSensitiveHeaders. The redirect loop initializes the boolean once. When a destination fails the header-forwarding check against the original request, the loop sets it to true. Subsequent iterations keep the value. The header-copying function receives that accumulated decision whenever it builds the next request.
This matters because the copying function retains a copy of the initial request headers. It does not simply inherit whatever headers survived on the previous request. Retaining the original headers supports the client's broader redirect behavior, but it also means absence on one outgoing request is insufficient to prevent restoration later. The persistent flag determines whether the retained values remain eligible for copying.
The sensitive-header switch in this version covers Authorization, Www-Authenticate, Cookie, and Cookie2. Other initial headers still follow the copying path. Applications should not infer that the patch identifies every possible application-specific secret by its header name. A custom credential header needs an application policy because its significance is unavailable to that switch.
The regression test specifies the history
The tagged client regression test goes further than the announcement's two redirects. TestClientStripHeadersOnRepeatedRedirect sends a request to a.example.com/, redirects it to b.example.com/, redirects again within B, and finally sends it back to a.example.com/redirect. Every request after the original must lack Authorization.
The return to A makes the intended rule precise. Removal persists even when the chain returns to the original domain. This avoids treating a return destination as permission to reconstruct the original authenticated request. The test checks the resulting requests, which is more informative than checking only that a particular comparison function returns false.
The forwarding helper also documents that a permitted subdomain relationship can retain these headers. Consequently, this fix does not establish a universal rule that every host change strips authentication. It preserves the established forwarding policy while making a removal decision persist through the chain. Cookie-jar behavior is separate from copying a Cookie header that the caller explicitly supplied, as the implementation comments explain.
A service that does not want automatic redirects can express that policy with the existing callback. This fragment constructs such a client without sending a request:
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
The net/http import is required. The tagged implementation documents that ErrUseLastResponse returns the most recent response without following the redirect and leaves its body for the caller to close. That policy changes application behavior, so it is a separate choice from accepting the security patch. It can be appropriate when a caller must inspect a redirect before authorizing another destination.
URI constraints require the right kind of name
The certificate fix concerns URI subject alternative names under a constrained certificate chain. According to the announcement, a URI containing an IPv6 address with a zone ID could incorrectly satisfy a URI name constraint. The affected population is private PKI users whose certificates contain URIs. The announcement explicitly excludes web PKI certificates because they are not permitted to contain these URI names.
The Go 1.23.5 verifier explains the required behavior in matchURIConstraint. Its comment cites RFC 5280's rule that a constrained URI must contain an authority whose host is a fully qualified domain name. An IP address is not an acceptable substitute for that domain name. The verifier therefore needs to classify the host before applying domain-constraint matching.
The implementation rejects an empty host and separates a port when necessary. It then rejects a host if netip.ParseAddr recognizes it as an address or if it has the bracketed form of an IPv6 literal. That second condition matters because the address parser rejects URI-style square brackets. Treating a failed address parse as proof that the input is a domain name would leave a gap between the two representations.
Only after those checks does the verifier call the domain-constraint matcher. The practical requirement is to reject the wrong name category before evaluating whether a suffix or domain relationship fits. A syntactically unusual address must not enter a domain-name comparison merely because one parser declines it.
I remain strongly in favor of putting these checks in the standard library. Their correctness depends on details that ordinary application code should not have to reproduce. For HTTP clients, the corrected implementation retains credential-removal history across the entire redirect chain. For private PKIs with URI identities, the verifier rejects IP-literal hosts before domain constraints can approve them.