swift: fix password being resent to another host on auth redirect GHSA-m97j-7x65-wh93 CVE-PENDING

A v2 or v3 auth request posts the user's password, or the application
credential secret, to the auth server in a JSON body which net/http
can rewind. On a 307 or 308 redirect to another host it resent that
body, password included, to the new host.

The transport now refuses to follow a redirect which leaves the host
and port of the original request if it would resend the request body.
This commit is contained in:
Nick Craig-Wood committed 2026-11-06 12:08:09 +00:00
1 parent 7b29032d4d
commit 544758e553
2 files changed
+55 -3

No files matched your search

+17 -3
View File
@@ -490,15 +490,22 @@ func (o *Object) split() (container, containerPath string) {
// forwarded when a redirect leaves the host of the original request.
var authHeaders = []string{"X-Auth-Token", "X-Auth-Key", "X-Auth-User"}
// errRedirectResendsBody is returned when following a redirect which
// leaves the host of the original request would resend that request's
// body, such as the password in a v2 or v3 auth request, to the new
// host.
var errRedirectResendsBody = errors.New("refusing to follow redirect which leaves the original host: would resend the request body")
// transport is the http.RoundTripper for the swift connection.
//
// The swift library makes its own http.Client with the default
// redirect policy and does not let the caller replace it, so the
// redirect policy is applied here at the transport level instead. A
// redirect which downgrades HTTPS to plaintext HTTP is refused with
// rest.ErrHTTPSDowngrade and the auth headers are removed from any
// hop once the redirect chain has left the host and port of the
// original request.
// rest.ErrHTTPSDowngrade. Once the redirect chain has left the host
// and port of the original request the auth headers are removed from
// every further hop, and a hop which would resend the request body is
// refused with errRedirectResendsBody.
type transport struct {
*fshttp.Transport
}
@@ -521,6 +528,13 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("redirect to %s: %w", req.URL.Redacted(), rest.ErrHTTPSDowngrade)
}
if leftHost {
// net/http resends the body on a 307 or 308 if it
// can be rewound, which for an auth request means
// the password, so those redirects stop here.
if req.Body != nil && req.Body != http.NoBody {
_ = req.Body.Close()
return nil, fmt.Errorf("redirect to %s leaves %s: %w", req.URL.Redacted(), origin.URL.Redacted(), errRedirectResendsBody)
}
for _, header := range authHeaders {
if req.Header.Get(header) == "" {
continue
+38
View File
@@ -2,6 +2,7 @@ package swift
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
@@ -236,3 +237,40 @@ func TestAuthRedirectStripsCredentialsOnCrossHost(t *testing.T) {
assert.Equal(t, int32(1), originHits.Load())
assert.Equal(t, int32(1), otherHits.Load())
}
// TestAuthRedirectRefusesToResendPassword checks a v2 or v3 auth
// request, which carries the password in its body, is not resent to
// another host the auth server redirects it to.
func TestAuthRedirectRefusesToResendPassword(t *testing.T) {
for _, authVersion := range []string{"2", "3"} {
t.Run("v"+authVersion, func(t *testing.T) {
var originHits, otherHits atomic.Int32
other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
otherHits.Add(1)
body, _ := io.ReadAll(r.Body)
assert.NotContains(t, string(body), "test-key", "password was resent to another host")
}))
defer other.Close()
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
originHits.Add(1)
body, _ := io.ReadAll(r.Body)
assert.Contains(t, string(body), "test-key")
http.Redirect(w, r, other.URL+r.URL.Path, http.StatusTemporaryRedirect)
}))
defer origin.Close()
m := configmap.Simple{
"auth": origin.URL + "/v" + authVersion,
"auth_version": authVersion,
"user": "test-user",
"key": "test-key",
"chunk_size": "5Gi",
}
_, err := NewFs(context.Background(), "swift-test", "container", m)
require.ErrorIs(t, err, errRedirectResendsBody)
assert.Equal(t, int32(1), originHits.Load())
assert.Equal(t, int32(0), otherHits.Load())
})
}
}