/
githubmirror
/
rclone
Обзор
Документация
Войти
/
githubmirror
/
rclone
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lib/rest/rest_test.go
146 строк
6 KB
Nick Craig-Wood
webdav: fix HTTPS to HTTP redirects leaking credentials GHSA-h4mf-4v27-hggj
31 июл 2026, 15:21
31 июл 2026, 15:21
59b513b
Код
Авторство
О чём код?
package rest import ( "context" "net/http" "net/http/httptest" "net/url" "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // mkRedirectReq makes a minimal *http.Request pointing at rawURL for // exercising the CheckRedirect functions. func mkRedirectReq(t *testing.T, rawURL, method string) *http.Request { u, err := url.Parse(rawURL) require.NoError(t, err) return &http.Request{URL: u, Method: method, Header: http.Header{}} } func TestPreserveMethodRedirectFn(t *testing.T) { t.Run("PreservesMethod", func(t *testing.T) { orig := mkRedirectReq(t, "https://example.com/a", "PROPFIND") next := mkRedirectReq(t, "https://example.com/b", "GET") require.NoError(t, PreserveMethodRedirectFn(next, []*http.Request{orig})) assert.Equal(t, "PROPFIND", next.Method) }) t.Run("RefusesDowngrade", func(t *testing.T) { orig := mkRedirectReq(t, "https://example.com/a", "PROPFIND") next := mkRedirectReq(t, "http://example.com/b", "PROPFIND") assert.ErrorIs(t, PreserveMethodRedirectFn(next, []*http.Request{orig}), ErrHTTPSDowngrade) }) t.Run("AllowsCrossHostHTTPS", func(t *testing.T) { orig := mkRedirectReq(t, "https://example.com/a", "PROPFIND") next := mkRedirectReq(t, "https://other.example.com/b", "PROPFIND") assert.NoError(t, PreserveMethodRedirectFn(next, []*http.Request{orig})) }) t.Run("AllowsPlainHTTP", func(t *testing.T) { orig := mkRedirectReq(t, "http://example.com/a", "PROPFIND") next := mkRedirectReq(t, "http://example.com/b", "PROPFIND") assert.NoError(t, PreserveMethodRedirectFn(next, []*http.Request{orig})) }) t.Run("TooManyRedirects", func(t *testing.T) { next := mkRedirectReq(t, "https://example.com/b", "GET") via := make([]*http.Request, 10) assert.Error(t, PreserveMethodRedirectFn(next, via)) }) } func TestRefuseHTTPSDowngradeRedirectFn(t *testing.T) { t.Run("RefusesDowngrade", func(t *testing.T) { orig := mkRedirectReq(t, "https://example.com/a", "GET") next := mkRedirectReq(t, "http://example.com/b", "GET") assert.ErrorIs(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig}), ErrHTTPSDowngrade) }) t.Run("AllowsUpgrade", func(t *testing.T) { orig := mkRedirectReq(t, "http://example.com/a", "GET") next := mkRedirectReq(t, "https://example.com/b", "GET") assert.NoError(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig})) }) t.Run("AllowsSameScheme", func(t *testing.T) { orig := mkRedirectReq(t, "https://example.com/a", "GET") next := mkRedirectReq(t, "https://example.com/b", "GET") assert.NoError(t, RefuseHTTPSDowngradeRedirectFn(next, []*http.Request{orig})) }) t.Run("TooManyRedirects", func(t *testing.T) { next := mkRedirectReq(t, "https://example.com/b", "GET") via := make([]*http.Request, 10) assert.Error(t, RefuseHTTPSDowngradeRedirectFn(next, via)) }) } // newDowngradeServers returns an HTTPS server that redirects every // request to a plaintext HTTP server on the same host, together with a // flag that records whether the plaintext server ever received an // Authorization header. Use tlsSrv.Client() for a client that trusts the // test certificate. func newDowngradeServers(t *testing.T) (tlsSrv *httptest.Server, sawAuth *atomic.Bool) { t.Helper() sawAuth = new(atomic.Bool) // Plaintext HTTP target that records whether it received credentials. target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "" { sawAuth.Store(true) } w.WriteHeader(http.StatusOK) })) t.Cleanup(target.Close) // HTTPS server that redirects to the plaintext target on the same host. tlsSrv = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL+r.URL.Path, http.StatusTemporaryRedirect) })) t.Cleanup(tlsSrv.Close) return tlsSrv, sawAuth } // TestRefuseHTTPSDowngradeRedirectEndToEnd drives a real HTTPS-to-HTTP // redirect through rest.Client with credentials set via SetUserPass, the // same way the webdav backend does. It shows that the default client // leaks credentials to the plaintext hop, whereas the redirect handlers // refuse to follow the downgrade and the plaintext hop never sees them. func TestRefuseHTTPSDowngradeRedirectEndToEnd(t *testing.T) { ctx := context.Background() // Baseline: without any check the credentials set by SetUserPass are // forwarded over the plaintext hop, which is the vulnerability. t.Run("DefaultLeaks", func(t *testing.T) { tlsSrv, sawAuth := newDowngradeServers(t) api := NewClient(tlsSrv.Client()).SetRoot(tlsSrv.URL) api.SetUserPass("user", "pass") _, err := api.Call(ctx, &Opts{Method: "GET", Path: "/file", NoResponse: true}) require.NoError(t, err) assert.True(t, sawAuth.Load(), "expected credentials to be sent over plaintext") }) // Default path (all normal webdav calls): the client refuses the // downgrade so nothing is sent. t.Run("DefaultPathRefused", func(t *testing.T) { tlsSrv, sawAuth := newDowngradeServers(t) client := tlsSrv.Client() client.CheckRedirect = RefuseHTTPSDowngradeRedirectFn api := NewClient(client).SetRoot(tlsSrv.URL) api.SetUserPass("user", "pass") _, err := api.Call(ctx, &Opts{Method: "GET", Path: "/file", NoResponse: true}) require.Error(t, err) assert.ErrorIs(t, err, ErrHTTPSDowngrade) assert.False(t, sawAuth.Load(), "plaintext hop must not receive credentials") }) // PROPFIND path (readMetaDataForPath): the per-call CheckRedirect // refuses the downgrade too. t.Run("PropfindPathRefused", func(t *testing.T) { tlsSrv, sawAuth := newDowngradeServers(t) api := NewClient(tlsSrv.Client()).SetRoot(tlsSrv.URL) api.SetUserPass("user", "pass") _, err := api.Call(ctx, &Opts{Method: "PROPFIND", Path: "/dir", NoResponse: true, CheckRedirect: PreserveMethodRedirectFn}) require.Error(t, err) assert.ErrorIs(t, err, ErrHTTPSDowngrade) assert.False(t, sawAuth.Load(), "plaintext hop must not receive credentials") }) }