diff --git a/backend/b2/b2.go b/backend/b2/b2.go index 22c5e079a..1b2515ed9 100644 --- a/backend/b2/b2.go +++ b/backend/b2/b2.go @@ -191,6 +191,11 @@ If the custom endpoint rewrites the requests for authentication, e.g., in Cloudflare Workers, this header needs to be handled properly. Leave blank if you want to use the endpoint provided by Backblaze. +Rclone will not follow a redirect from this endpoint which downgrades +HTTPS to HTTP, and will not send the "Authorization" header or the +SSE-C key headers once a redirect has left the host and port of this +endpoint. + The URL provided here SHOULD have the protocol and SHOULD NOT have a trailing slash or specify the /file/bucket subpath as rclone will request files with "{download_url}/file/{bucket_name}/{path}". @@ -529,6 +534,25 @@ func (f *Fs) setRoot(root string) { f.rootBucket, f.rootDirectory = bucket.Split(f.root) } +// redirectSecretHeaders are the request headers carrying secrets that +// must not be forwarded when a redirect leaves the original host. +var redirectSecretHeaders = []string{ + "Authorization", + sseAlgorithmHeader, + sseKeyHeader, + sseMd5Header, +} + +// newClient makes the http client used for all B2 requests. +// +// The download_url option sends the requests to a host the user does +// not control so it strips headers on cross host redirects. +func newClient(ctx context.Context) *http.Client { + client := fshttp.NewClient(ctx) + client.CheckRedirect = rest.StripHeadersOnCrossHostRedirectFn(redirectSecretHeaders...) + return client +} + // NewFs constructs an Fs from the path, bucket:path func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) { // Parse config into Options struct @@ -581,7 +605,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e name: name, opt: *opt, ci: ci, - srv: rest.NewClient(fshttp.NewClient(ctx)).SetErrorHandler(errorHandler), + srv: rest.NewClient(newClient(ctx)).SetErrorHandler(errorHandler), cache: bucket.NewCache(), _bucketID: make(map[string]string, 1), _bucketType: make(map[string]string, 1), diff --git a/backend/b2/b2_internal_test.go b/backend/b2/b2_internal_test.go index 2579928be..f4de7c560 100644 --- a/backend/b2/b2_internal_test.go +++ b/backend/b2/b2_internal_test.go @@ -4,6 +4,8 @@ import ( "context" "crypto/sha1" "fmt" + "net/http" + "net/http/httptest" "path" "sort" "strings" @@ -617,3 +619,60 @@ func (f *Fs) InternalTest(t *testing.T) { } var _ fstests.InternalTester = (*Fs)(nil) + +// redirectTestHeaders is a deliberately literal copy of +// redirectSecretHeaders so that a header dropped from the production +// list fails TestRedirectSecretHeaders rather than silently losing +// test coverage. +var redirectTestHeaders = map[string]string{ + "Authorization": "secret-token", + "X-Bz-Server-Side-Encryption-Customer-Algorithm": "AES256", + "X-Bz-Server-Side-Encryption-Customer-Key": "secret-key", + "X-Bz-Server-Side-Encryption-Customer-Key-Md5": "secret-md5", +} + +func TestRedirectSecretHeaders(t *testing.T) { + want := make([]string, 0, len(redirectTestHeaders)) + for header := range redirectTestHeaders { + want = append(want, header) + } + sort.Strings(want) + got := append([]string(nil), redirectSecretHeaders...) + sort.Strings(got) + assert.Equal(t, want, got) +} + +// redirectTestRequest makes a GET request to rawURL carrying the +// secret headers plus a harmless marker header. +func redirectTestRequest(t *testing.T, rawURL string) *http.Request { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil) + require.NoError(t, err) + for header, value := range redirectTestHeaders { + req.Header.Set(header, value) + } + req.Header.Set("X-Bz-Test-Mode", "marker") + return req +} + +func TestClientStripsSecretHeadersOnCrossHostRedirect(t *testing.T) { + client := newClient(context.Background()) + + redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for header := range redirectTestHeaders { + assert.Empty(t, r.Header.Get(header), "%s should have been stripped", header) + } + assert.Equal(t, "marker", r.Header.Get("X-Bz-Test-Mode")) + w.WriteHeader(http.StatusOK) + })) + defer redirectServer.Close() + + initialServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, redirectServer.URL, http.StatusTemporaryRedirect) + })) + defer initialServer.Close() + + resp, err := client.Do(redirectTestRequest(t, initialServer.URL)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.NoError(t, resp.Body.Close()) +}