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 <terrylhowe@gmail.com>
pull/32202/head
Terry Howe 7 days ago
parent acc867b88e
commit 2fb05f8a35
No known key found for this signature in database

@ -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) {

@ -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 {

Loading…
Cancel
Save