From 2fb05f8a35f6c0d5760c03c12d60c46adf2b3d12 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 10 Jun 2026 18:53:09 -0600 Subject: [PATCH] fix(registry): keep credentials on plain-HTTP fallback with oras-go v2.6.1 oras-go v2.6.1 hardens the auth client to drop the Authorization header when a request's origin changes mid-flight (GHSA-vh4v-2xq2-g5cg). Helm's fallbackTransport reaches plain-HTTP registries by downgrading the connection from https to http inside a single round trip, which oras now treats as a credential-leaking origin change and refuses to authenticate. On login, detect when the transport has fallen back to plain HTTP and, in that case, set PlainHTTP explicitly and re-ping so requests are built as http from the start. The scheme no longer changes mid-request, credentials flow as before, and the new cross-origin protection is preserved for real https registries (forcedHTTP stays false, so the retry never triggers). Signed-off-by: Terry Howe --- pkg/registry/client.go | 43 ++++++++++++++++++++++++++++++++++------ pkg/registry/fallback.go | 8 ++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index b14b767b2..03ff2c9a9 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -296,15 +296,22 @@ func (c *Client) Login(host string, options ...LoginOption) error { } reg.PlainHTTP = c.plainHTTP cred := auth.Credential{Username: c.username, Password: c.password} - c.authorizer.ForceAttemptOAuth2 = true reg.Client = c.authorizer ctx := context.Background() - if err := reg.Ping(ctx); err != nil { - c.authorizer.ForceAttemptOAuth2 = false - if err := reg.Ping(ctx); err != nil { - return fmt.Errorf("authenticating to %q: %w", host, err) - } + err = c.ping(ctx, reg) + if err != nil && !reg.PlainHTTP && c.forcedHTTP() { + // The registry is plain HTTP: the fallback transport downgraded the + // connection from https to http. ORAS v2.6.1+ refuses to forward + // credentials across that implicit scheme change (GHSA-vh4v-2xq2-g5cg), + // so the credentialed ping above fails. Now that the fallback has been + // detected, set PlainHTTP explicitly and retry so requests are built as + // http from the start and the scheme no longer changes mid-request. + reg.PlainHTTP = true + err = c.ping(ctx, reg) + } + if err != nil { + return fmt.Errorf("authenticating to %q: %w", host, err) } // The credentialsStore loader does not handle empty files. So, there is a workaround. @@ -341,6 +348,30 @@ func (c *Client) Login(host string, options ...LoginOption) error { return nil } +// ping authenticates against the registry, first attempting the OAuth2 token +// flow and falling back to the basic/refresh token flow on failure. +func (c *Client) ping(ctx context.Context, reg *remote.Registry) error { + c.authorizer.ForceAttemptOAuth2 = true + err := reg.Ping(ctx) + if err != nil { + c.authorizer.ForceAttemptOAuth2 = false + err = reg.Ping(ctx) + } + return err +} + +// forcedHTTP reports whether the client's transport has fallen back to plain +// HTTP after a failed HTTPS attempt, indicating the registry is plain HTTP. +func (c *Client) forcedHTTP() bool { + if c.httpClient == nil { + return false + } + if ft, ok := c.httpClient.Transport.(*fallbackTransport); ok { + return ft.forcedHTTP() + } + return false +} + // LoginOptBasicAuth returns a function that sets the username/password settings on login func LoginOptBasicAuth(username string, password string) LoginOption { return func(o *loginOperation) { diff --git a/pkg/registry/fallback.go b/pkg/registry/fallback.go index 1db729576..a5ca6f72c 100644 --- a/pkg/registry/fallback.go +++ b/pkg/registry/fallback.go @@ -38,6 +38,14 @@ func newTransport(debug bool) *fallbackTransport { } } +// forcedHTTP reports whether the transport has fallen back to plain HTTP after +// a failed HTTPS attempt. Once this is true, the registry is known to be plain +// HTTP and callers should set PlainHTTP so requests are built as http from the +// start (see Client.Login). +func (t *fallbackTransport) forcedHTTP() bool { + return t.forceHTTP.Load() +} + // RoundTrip wraps base round trip with conditional insecure retry. func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) { if ok := t.forceHTTP.Load(); ok {