Skip to content

Commit 7bb91b5

Browse files
committed
fix/security: strip auth headers on cross-host redirect
1 parent b917d53 commit 7bb91b5

2 files changed

Lines changed: 133 additions & 1 deletion

File tree

internal/api/api.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ func NewClient(opts ClientOpts) Client {
139139
transport := buildTransport(opts, flags)
140140

141141
httpClient := &http.Client{
142-
Transport: transport,
142+
Transport: transport,
143+
CheckRedirect: checkRedirect,
143144
}
144145

145146
return &client{
@@ -155,6 +156,13 @@ func NewClient(opts ClientOpts) Client {
155156
}
156157
}
157158

159+
func checkRedirect(req *http.Request, via []*http.Request) error {
160+
if len(via) > 0 && req.URL.Host != via[0].URL.Host {
161+
return http.ErrUseLastResponse
162+
}
163+
return nil
164+
}
165+
158166
func (c *client) checkIfCIAccessTokenRequired() error {
159167
if c.opts.RequireAccessTokenInCI && c.opts.AccessToken == "" {
160168
return ErrCIAccessTokenRequired

internal/api/redirect_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"sync/atomic"
10+
"testing"
11+
"time"
12+
13+
"github.com/sourcegraph/src-cli/internal/oauth"
14+
)
15+
16+
func TestCredentialsAreNotSentOnCrossHostRedirect(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
header string
20+
value string
21+
configure func(*ClientOpts, string)
22+
}{
23+
{
24+
name: "OAuth token",
25+
header: "Authorization",
26+
value: "Bearer oauth-secret-token",
27+
configure: func(opts *ClientOpts, endpoint string) {
28+
opts.OAuthToken = &oauth.Token{
29+
Endpoint: endpoint,
30+
AccessToken: "oauth-secret-token",
31+
ExpiresAt: time.Now().Add(time.Hour),
32+
}
33+
},
34+
},
35+
{
36+
name: "custom auth-proxy header",
37+
header: "X-Dbx-Auth-Token",
38+
value: "proxy-secret-token",
39+
configure: func(opts *ClientOpts, _ string) {
40+
opts.AdditionalHeaders = map[string]string{
41+
"X-Dbx-Auth-Token": "proxy-secret-token",
42+
}
43+
},
44+
},
45+
}
46+
47+
for _, test := range tests {
48+
t.Run(test.name, func(t *testing.T) {
49+
var redirectTargetRequests atomic.Int32
50+
redirectTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
redirectTargetRequests.Add(1)
52+
w.WriteHeader(http.StatusOK)
53+
}))
54+
defer redirectTarget.Close()
55+
56+
var initialHeader string
57+
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
58+
initialHeader = r.Header.Get(test.header)
59+
http.Redirect(w, r, redirectTarget.URL, http.StatusFound)
60+
}))
61+
defer redirector.Close()
62+
63+
endpointURL, err := url.Parse(redirector.URL)
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
opts := ClientOpts{EndpointURL: endpointURL, Out: io.Discard}
68+
test.configure(&opts, redirector.URL)
69+
client := NewClient(opts)
70+
71+
req, err := client.NewHTTPRequest(context.Background(), http.MethodGet, "", nil)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
resp, err := client.Do(req)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
defer resp.Body.Close()
80+
81+
if resp.StatusCode != http.StatusFound {
82+
t.Fatalf("got status %d, want %d", resp.StatusCode, http.StatusFound)
83+
}
84+
if initialHeader != test.value {
85+
t.Fatalf("initial request %s header = %q, want %q", test.header, initialHeader, test.value)
86+
}
87+
if got := redirectTargetRequests.Load(); got != 0 {
88+
t.Fatalf("cross-host redirect target received %d requests, want 0", got)
89+
}
90+
})
91+
}
92+
}
93+
94+
func TestClientFollowsSameHostRedirect(t *testing.T) {
95+
mux := http.NewServeMux()
96+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
97+
http.Redirect(w, r, "/target", http.StatusFound)
98+
})
99+
mux.HandleFunc("/target", func(w http.ResponseWriter, r *http.Request) {
100+
w.WriteHeader(http.StatusNoContent)
101+
})
102+
server := httptest.NewServer(mux)
103+
defer server.Close()
104+
105+
endpointURL, err := url.Parse(server.URL)
106+
if err != nil {
107+
t.Fatal(err)
108+
}
109+
client := NewClient(ClientOpts{EndpointURL: endpointURL, Out: io.Discard})
110+
111+
req, err := client.NewHTTPRequest(context.Background(), http.MethodGet, "", nil)
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
resp, err := client.Do(req)
116+
if err != nil {
117+
t.Fatal(err)
118+
}
119+
defer resp.Body.Close()
120+
121+
if resp.StatusCode != http.StatusNoContent {
122+
t.Fatalf("got status %d, want %d", resp.StatusCode, http.StatusNoContent)
123+
}
124+
}

0 commit comments

Comments
 (0)