Conversation
applyDefaults installed the safe client only when cfg.HTTPClient was nil, so a caller who supplied one kept their transport and lost CheckRedirect entirely. Measured before: applyDefaults with a supplied client returned CheckRedirect nil, the default path returned it set. safeRedirect is four controls, not one: a five hop cap, https on every redirect target, refusal of a loopback, private, link-local or unspecified address, and refusal of a cross-origin redirect. All four went together, on the token exchange, JWKS, userinfo and discovery fetches. The token exchange carries the client secret and receives the ID token, so those are the fetches that least tolerate an unguarded redirect. A caller supplies a client for its transport, its timeout or its proxy. None of those intentions include turning off SSRF protection, and nothing told them it had happened. The security standard is explicit that a caller opts in to looseness and never to safety. applyDefaults now shallow copies a supplied client and forces the policy on the copy. The copy is the second half of the fix: forcing it onto the caller own *http.Client would change how that client behaves everywhere else in their program, which is a different surprise rather than a fix. They keep Transport, Timeout and Jar. isLoopbackHost goes with it. It matched three literals, so a development server on 127.0.0.2 was refused the documented plaintext exception. That failed closed and was a usability gap, but the package already had the right definition a few lines away in isPrivateHost, and the two disagreed about what loopback means. It uses net.IP.IsLoopback now, and the tests pin that the exception does not widen past loopback: 10.0.0.1, 192.168.1.1 and localhost.evil.com stay refused. Both halves checked in the failing direction. Removing the forcing fails the guard test; mutating the caller client instead of copying fails the no-mutation test. Closes #364 Closes #365 Signed-off-by: Jose <75870284+Jaro-c@users.noreply.github.com>
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.
applyDefaultsinstalled the safe client only whencfg.HTTPClientwas nil, so a caller who supplied one kept their transport and lostCheckRedirectentirely.Measured on develop at 6396536:
applyDefaults(Config{HTTPClient: &http.Client{}})returnsCheckRedirect == nil,applyDefaults(Config{})returns it set.safeRedirectis four controls, not one:All four went together, on every fetch the package makes: token exchange, JWKS, userinfo, discovery. The token exchange carries the client secret and receives the ID token, so those are the fetches that least tolerate an unguarded redirect.
A caller supplies a client for its transport, its timeout or its proxy. None of those intentions include turning off SSRF protection, and nothing told them it had happened.
standards/securityis explicit that the most restrictive configuration is the active one and that users opt in to looseness, never to safety.The copy is the second half of the fix
applyDefaultsnow shallow copies a supplied client and forces the policy on the copy. Forcing it onto the caller own*http.Clientwould change how that client behaves everywhere else in their program, which is a different surprise rather than a fix. They keepTransport,TimeoutandJar, which is what they supplied it for.Both halves are checked in the failing direction, separately: removing the forcing fails the guard test, and mutating the caller client instead of copying fails the no-mutation test.
isLoopbackHost, riding along
It matched three literals, so a development server bound to
127.0.0.2to dodge a port collision was refused the documented plaintext exception. That fails closed, so it was a usability gap rather than a hole, but the package already had the right definition a few lines away:isPrivateHostusesnet.IP.IsLoopback()and the two disagreed about what loopback means.It uses
net.IP.IsLoopback()now. The tests pin that the exception does not widen past loopback:10.0.0.1,192.168.1.1andlocalhost.evil.comstay refused.Provenance
Both found by a MiniMax-M3 pass over the package. It rated the client one a hardening suggestion rather than a defect, on the grounds that the behaviour is documented in a doc comment. I disagree and filed it as a defect: a one-line comment in a long file is not how a caller learns that four security controls just turned off, and the standard treats a disabled security default as a defect regardless of whether it is written down.
Closes #364
Closes #365