Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cs.opts.ErrorHandler.ServeHTTP(w, r)
return
}
if !sameOrigin(&requestURL, parsedOrigin) && !slices.Contains(cs.opts.TrustedOrigins, parsedOrigin.Host) {
if !sameOrigin(&requestURL, parsedOrigin) && !slices.Contains(cs.opts.TrustedOrigins, parsedOrigin.Host) && !sameOriginFetch(r) {
r = envError(r, ErrBadOrigin)
cs.opts.ErrorHandler.ServeHTTP(w, r)
return
Expand Down Expand Up @@ -318,7 +318,7 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If the request is being served via TLS and the Referer is not the
// same origin, check the domain against our allowlist. We only
// check when we have host information from the referer.
if referer.Host != "" && referer.Host != r.Host && !slices.Contains(cs.opts.TrustedOrigins, referer.Host) {
if referer.Host != "" && referer.Host != r.Host && !slices.Contains(cs.opts.TrustedOrigins, referer.Host) && !sameOriginFetch(r) {
r = envError(r, ErrBadReferer)
cs.opts.ErrorHandler.ServeHTTP(w, r)
return
Expand Down
82 changes: 82 additions & 0 deletions csrf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,85 @@ func createRequest(method, path string, useTLS bool) *http.Request {
}
return r
}

// TestReverseProxyRewrittenHost covers gorilla/csrf#187: a same-origin
// browser POST whose Host was rewritten by a reverse proxy must still
// pass when Sec-Fetch-Site is same-origin. Cross-site and missing
// Sec-Fetch-Site must not.
func TestReverseProxyRewrittenHost(t *testing.T) {
tests := []struct {
name string
origin string
referer string
secFetchSite string
want bool
}{
{
name: "Origin mismatch with Sec-Fetch-Site same-origin pass",
origin: "https://www.gorillatoolkit.org",
secFetchSite: "same-origin",
want: true,
},
{
name: "Origin mismatch without Sec-Fetch-Site reject",
origin: "https://www.gorillatoolkit.org",
},
{
name: "Origin mismatch with Sec-Fetch-Site cross-site reject",
origin: "https://www.gorillatoolkit.org",
secFetchSite: "cross-site",
},
{
name: "Referer mismatch with Sec-Fetch-Site same-origin pass",
referer: "https://www.gorillatoolkit.org/form",
secFetchSite: "same-origin",
want: true,
},
{
name: "Referer mismatch without Sec-Fetch-Site reject",
referer: "https://www.gorillatoolkit.org/form",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var token string
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
token = Token(r)
}))
mux.Handle("/submit", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
p := Protect(testKey)(mux)

r := createRequest("GET", "/", true)
rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)

r = createRequest("POST", "/submit", true)
r.Host = "backend.acme.example"
if tt.origin != "" {
r.Header.Set("Origin", tt.origin)
}
if tt.referer != "" {
r.Header.Set("Referer", tt.referer)
}
if tt.secFetchSite != "" {
r.Header.Set("Sec-Fetch-Site", tt.secFetchSite)
}
setCookie(rr, r)
r.Header.Set("X-CSRF-Token", token)

rr = httptest.NewRecorder()
p.ServeHTTP(rr, r)

if tt.want && rr.Code != http.StatusOK {
t.Fatalf("middleware failed to pass rewritten-Host request: got %v want %v",
rr.Code, http.StatusOK)
}
if !tt.want && rr.Code != http.StatusForbidden {
t.Fatalf("middleware failed to reject rewritten-Host request: got %v want %v",
rr.Code, http.StatusForbidden)
}
})
}
}
8 changes: 8 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ func sameOrigin(a, b *url.URL) bool {
return (a.Scheme == b.Scheme && a.Host == b.Host)
}

// sameOriginFetch reports whether the browser labeled the request as
// same-origin via Sec-Fetch-Site. Browsers set this forbidden header;
// scripts cannot. Reverse proxies that rewrite Host still leave
// Sec-Fetch-Site=same-origin on genuine same-origin form posts.
func sameOriginFetch(r *http.Request) bool {
return r.Header.Get("Sec-Fetch-Site") == "same-origin"
}

// compare securely (constant-time) compares the unmasked token from the request
// against the real token from the session.
func compareTokens(a, b []byte) bool {
Expand Down
Loading