fshttp: don't send --header values to other hosts on redirect GHSA-486v-q2wf-fp2r CVE-PENDING
The headers set with --header and --header-download are added to every request by the rclone transport, including redirect hops which net/http makes to other hosts, so a credential passed with --header for one host could be sent to any host that server chose to redirect to. The transport now walks the redirect chain net/http records on each redirected request and, once the chain has visited a host other than the one originally requested, removes the headers rather than adding them. Also restore the global --client-cert and --client-key config after TestCertificates so its temporary files are not used by later tests.
This commit is contained in:
+24
-1
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
"github.com/rclone/rclone/fs/accounting"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
|
"github.com/rclone/rclone/lib/rest"
|
||||||
"github.com/rclone/rclone/lib/structs"
|
"github.com/rclone/rclone/lib/structs"
|
||||||
"github.com/youmark/pkcs8"
|
"github.com/youmark/pkcs8"
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
@@ -591,6 +592,22 @@ func newClientTrace(req *http.Request) *httptrace.ClientTrace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirectLeavesHost reports whether req is a redirect hop in a
|
||||||
|
// chain which has visited a host other than that of the original
|
||||||
|
// request at any point.
|
||||||
|
func redirectLeavesHost(req *http.Request) bool {
|
||||||
|
origin := req
|
||||||
|
for origin.Response != nil && origin.Response.Request != nil {
|
||||||
|
origin = origin.Response.Request
|
||||||
|
}
|
||||||
|
for hop := req; hop != origin; hop = hop.Response.Request {
|
||||||
|
if !rest.SameHost(hop.URL, origin.URL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// RoundTrip implements the RoundTripper interface.
|
// RoundTrip implements the RoundTripper interface.
|
||||||
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
// Check if certificates are being used and the certificates are expired
|
// Check if certificates are being used and the certificates are expired
|
||||||
@@ -602,10 +619,16 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error
|
|||||||
accounting.LimitTPS(req.Context())
|
accounting.LimitTPS(req.Context())
|
||||||
// Force user agent
|
// Force user agent
|
||||||
req.Header.Set("User-Agent", t.userAgent)
|
req.Header.Set("User-Agent", t.userAgent)
|
||||||
// Set user defined headers
|
// Set user defined headers, unless redirected elsewhere
|
||||||
|
if redirectLeavesHost(req) {
|
||||||
|
for _, option := range t.headers {
|
||||||
|
req.Header.Del(option.Key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for _, option := range t.headers {
|
for _, option := range t.headers {
|
||||||
req.Header.Set(option.Key, option.Value)
|
req.Header.Set(option.Key, option.Value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Filter the request if required
|
// Filter the request if required
|
||||||
if t.filterRequest != nil {
|
if t.filterRequest != nil {
|
||||||
t.filterRequest(req)
|
t.filterRequest(req)
|
||||||
|
|||||||
@@ -174,6 +174,12 @@ func TestCertificates(t *testing.T) {
|
|||||||
// create a test cert/key pair and write it to the files
|
// create a test cert/key pair and write it to the files
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
ci := fs.GetConfig(ctx)
|
ci := fs.GetConfig(ctx)
|
||||||
|
// Restore the global config for later tests as the temp files
|
||||||
|
// are removed when this test finishes
|
||||||
|
oldCert, oldKey := ci.ClientCert, ci.ClientKey
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ci.ClientCert, ci.ClientKey = oldCert, oldKey
|
||||||
|
})
|
||||||
// Create a test certificate and write it to a temp file
|
// Create a test certificate and write it to a temp file
|
||||||
ci.ClientCert = t.TempDir() + "client.cert"
|
ci.ClientCert = t.TempDir() + "client.cert"
|
||||||
ci.ClientKey = t.TempDir() + "client.key"
|
ci.ClientKey = t.TempDir() + "client.key"
|
||||||
@@ -202,3 +208,59 @@ func TestCertificates(t *testing.T) {
|
|||||||
_, err = client.Get(ts.URL)
|
_, err = client.Get(ts.URL)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRedirectStripsGlobalHeaders checks the headers set with --header
|
||||||
|
// are sent to the requested host and any redirect on it but not to
|
||||||
|
// another host the request is redirected to
|
||||||
|
func TestRedirectStripsGlobalHeaders(t *testing.T) {
|
||||||
|
var got http.Header
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
got = r.Header.Clone()
|
||||||
|
}))
|
||||||
|
defer target.Close()
|
||||||
|
|
||||||
|
var ts *httptest.Server
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/same":
|
||||||
|
http.Redirect(w, r, ts.URL+"/moved", http.StatusFound)
|
||||||
|
case "/other":
|
||||||
|
http.Redirect(w, r, target.URL+"/moved", http.StatusFound)
|
||||||
|
case "/back":
|
||||||
|
// Chain via the other host and back again
|
||||||
|
http.Redirect(w, r, target.URL+"/bounce", http.StatusFound)
|
||||||
|
default:
|
||||||
|
got = r.Header.Clone()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
// The other host bounces /bounce back to the original host
|
||||||
|
target.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/bounce" {
|
||||||
|
http.Redirect(w, r, ts.URL+"/moved", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got = r.Header.Clone()
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, ci := fs.AddConfig(context.Background())
|
||||||
|
ci.Headers = []*fs.HTTPOption{{Key: "X-Potato", Value: "sausage"}}
|
||||||
|
client := NewClient(ctx)
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"/direct", "sausage"},
|
||||||
|
{"/same", "sausage"},
|
||||||
|
{"/other", ""},
|
||||||
|
{"/back", ""},
|
||||||
|
} {
|
||||||
|
got = nil
|
||||||
|
resp, err := client.Get(ts.URL + test.path)
|
||||||
|
require.NoError(t, err, test.path)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.NotNil(t, got, test.path)
|
||||||
|
assert.Equal(t, test.want, got.Get("X-Potato"), test.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user