CVE-2025-24358
Description
gorilla/csrf provides Cross Site Request Forgery (CSRF) prevention middleware for Go web applications & services. Prior to 1.7.2, gorilla/csrf does not validate the Origin header against an allowlist. Its executes its validation of the Referer header for cross-origin requests only when it believes the request is being served over TLS. It determines this by inspecting the r.URL.Scheme value. However, this value is never populated for "server" requests per the Go spec, and so this check does not run in practice. This vulnerability allows an attacker who has gained XSS on a subdomain or top level domain to perform authenticated form submissions against gorilla/csrf protected targets that share the same top level domain. This vulnerability is fixed in 1.7.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
gorilla/csrf prior to 1.7.2 does not validate Origin header and its Referer check for HTTPS never runs because r.URL.Scheme is empty for server requests, allowing CSRF from same top-level domain subdomains.
Vulnerability
gorilla/csrf is a CSRF protection middleware for Go web applications. Prior to version 1.7.2, it fails to validate the Origin header against an allowlist. Its intended Referer validation only executes for requests it believes are served over TLS, determined by inspecting r.URL.Scheme. However, in Go's net/http server, r.URL.Scheme is never populated for incoming requests, so the Referer check never actually runs [1][2]. This renders the CSRF protection ineffective for cross-origin requests.
Exploitation
An attacker who has gained XSS on any subdomain (or the top-level domain) of a site using gorilla/csrf can perform authenticated form submissions against protected endpoints. The attacker can exfiltrate the CSRF token and cookie from the target, then set a cookie with a broader path (e.g., /submit) so that it is sent by the browser when submitting a form from the attacker's origin. Since the Referer check is disabled, the malicious submission is accepted as legitimate [4].
Impact
Successful exploitation allows the attacker to perform state-changing actions (e.g., password change, financial transaction) on behalf of an authenticated victim, without proper origin validation. This qualifies as a Cross-Site Request Forgery vulnerability affecting the same top-level domain [2].
Mitigation
The vulnerability is fixed in gorilla/csrf 1.7.2. The fix adds validation of the Origin header against a trusted list and corrects the TLS detection logic by using request context to determine plaintext HTTP. Users should upgrade immediately. No workaround is available as the flawed check is integral to the middleware [3][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/gorilla/csrfGo | < 1.7.3 | 1.7.3 |
Affected products
1Patches
19dd6af1f6d30Merge commit from fork
3 files changed · +307 −131
csrf.go+75 −14 modified@@ -1,10 +1,12 @@ package csrf import ( + "context" "errors" "fmt" "net/http" "net/url" + "slices" "github.com/gorilla/securecookie" ) @@ -22,6 +24,14 @@ const ( errorPrefix string = "gorilla/csrf: " ) +type contextKey string + +// PlaintextHTTPContextKey is the context key used to store whether the request +// is being served via plaintext HTTP. This is used to signal to the middleware +// that strict Referer checking should not be enforced as is done for HTTPS by +// default. +const PlaintextHTTPContextKey contextKey = "plaintext" + var ( // The name value used in form fields. fieldName = tokenKey @@ -41,6 +51,9 @@ var ( // ErrNoReferer is returned when a HTTPS request provides an empty Referer // header. ErrNoReferer = errors.New("referer not supplied") + // ErrBadOrigin is returned when the Origin header is present and is not a + // trusted origin. + ErrBadOrigin = errors.New("origin invalid") // ErrBadReferer is returned when the scheme & host in the URL do not match // the supplied Referer header. ErrBadReferer = errors.New("referer invalid") @@ -242,10 +255,50 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) { // HTTP methods not defined as idempotent ("safe") under RFC7231 require // inspection. if !contains(safeMethods, r.Method) { - // Enforce an origin check for HTTPS connections. As per the Django CSRF - // implementation (https://goo.gl/vKA7GE) the Referer header is almost - // always present for same-domain HTTP requests. - if r.URL.Scheme == "https" { + var isPlaintext bool + val := r.Context().Value(PlaintextHTTPContextKey) + if val != nil { + isPlaintext, _ = val.(bool) + } + + // take a copy of the request URL to avoid mutating the original + // attached to the request. + // set the scheme & host based on the request context as these are not + // populated by default for server requests + // ref: https://pkg.go.dev/net/http#Request + requestURL := *r.URL // shallow clone + + requestURL.Scheme = "https" + if isPlaintext { + requestURL.Scheme = "http" + } + if requestURL.Host == "" { + requestURL.Host = r.Host + } + + // if we have an Origin header, check it against our allowlist + origin := r.Header.Get("Origin") + if origin != "" { + parsedOrigin, err := url.Parse(origin) + if err != nil { + r = envError(r, ErrBadOrigin) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + if !sameOrigin(&requestURL, parsedOrigin) && !slices.Contains(cs.opts.TrustedOrigins, parsedOrigin.Host) { + r = envError(r, ErrBadOrigin) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return + } + } + + // If we are serving via TLS and have no Origin header, prevent against + // CSRF via HTTP machine in the middle attacks by enforcing strict + // Referer origin checks. Consider an attacker who performs a + // successful HTTP Machine-in-the-Middle attack and uses this to inject + // a form and cause submission to our origin. We strictly disallow + // cleartext HTTP origins and evaluate the domain against an allowlist. + if origin == "" && !isPlaintext { // Fetch the Referer value. Call the error handler if it's empty or // otherwise fails to parse. referer, err := url.Parse(r.Referer()) @@ -255,18 +308,17 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - valid := sameOrigin(r.URL, referer) - - if !valid { - for _, trustedOrigin := range cs.opts.TrustedOrigins { - if referer.Host == trustedOrigin { - valid = true - break - } - } + // disallow cleartext HTTP referers when serving via TLS + if referer.Scheme == "http" { + r = envError(r, ErrBadReferer) + cs.opts.ErrorHandler.ServeHTTP(w, r) + return } - if !valid { + // 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) { r = envError(r, ErrBadReferer) cs.opts.ErrorHandler.ServeHTTP(w, r) return @@ -308,6 +360,15 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) { contextClear(r) } +// PlaintextHTTPRequest accepts as input a http.Request and returns a new +// http.Request with the PlaintextHTTPContextKey set to true. This is used to +// signal to the CSRF middleware that the request is being served over plaintext +// HTTP and that Referer-based origin allow-listing checks should be skipped. +func PlaintextHTTPRequest(r *http.Request) *http.Request { + ctx := context.WithValue(r.Context(), PlaintextHTTPContextKey, true) + return r.WithContext(ctx) +} + // unauthorizedhandler sets a HTTP 403 Forbidden status and writes the // CSRF failure reason to the response. func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
csrf_test.go+224 −101 modified@@ -1,6 +1,7 @@ package csrf import ( + "fmt" "net/http" "net/http/httptest" "strings" @@ -16,10 +17,7 @@ func TestProtect(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) - r, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", false) rr := httptest.NewRecorder() p := Protect(testKey)(s) @@ -46,10 +44,7 @@ func TestCookieOptions(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) - r, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", false) rr := httptest.NewRecorder() p := Protect(testKey, CookieName("nameoverride"), Secure(false), HttpOnly(false), Path("/pathoverride"), Domain("domainoverride"), MaxAge(173))(s) @@ -86,10 +81,7 @@ func TestMethods(t *testing.T) { // Test idempontent ("safe") methods for _, method := range safeMethods { - r, err := http.NewRequest(method, "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest(method, "/", false) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -107,10 +99,7 @@ func TestMethods(t *testing.T) { // Test non-idempotent methods (should return a 403 without a cookie set) nonIdempotent := []string{"POST", "PUT", "DELETE", "PATCH"} for _, method := range nonIdempotent { - r, err := http.NewRequest(method, "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest(method, "/", false) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -133,10 +122,7 @@ func TestNoCookie(t *testing.T) { p := Protect(testKey)(s) // POST the token back in the header. - r, err := http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("POST", "/", false) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -158,19 +144,13 @@ func TestBadCookie(t *testing.T) { })) // Obtain a CSRF cookie via a GET request. - r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", false) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. - r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r = createRequest("POST", "/", false) // Replace the cookie prefix badHeader := strings.Replace(cookieName+"=", rr.Header().Get("Set-Cookie"), "_badCookie", -1) @@ -193,10 +173,7 @@ func TestVaryHeader(t *testing.T) { s.HandleFunc("/", testHandler) p := Protect(testKey)(s) - r, err := http.NewRequest("HEAD", "https://www.golang.org/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", true) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -211,16 +188,13 @@ func TestVaryHeader(t *testing.T) { } } -// Requests with no Referer header should fail. +// TestNoReferer checks that HTTPS requests with no Referer header fail. func TestNoReferer(t *testing.T) { s := http.NewServeMux() s.HandleFunc("/", testHandler) p := Protect(testKey)(s) - r, err := http.NewRequest("POST", "https://golang.org/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("POST", "https://golang.org/", true) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -243,20 +217,12 @@ func TestBadReferer(t *testing.T) { })) // Obtain a CSRF cookie via a GET request. - r, err := http.NewRequest("GET", "https://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } - + r := createRequest("GET", "/", true) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. - r, err = http.NewRequest("POST", "https://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } - + r = createRequest("POST", "/", true) setCookie(rr, r) r.Header.Set("X-CSRF-Token", token) @@ -289,50 +255,47 @@ func TestTrustedReferer(t *testing.T) { } for _, item := range testTable { - s := http.NewServeMux() + t.Run(fmt.Sprintf("TrustedOrigin: %v", item.trustedOrigin), func(t *testing.T) { - p := Protect(testKey, TrustedOrigins(item.trustedOrigin))(s) + s := http.NewServeMux() - var token string - s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - token = Token(r) - })) + p := Protect(testKey, TrustedOrigins(item.trustedOrigin))(s) - // Obtain a CSRF cookie via a GET request. - r, err := http.NewRequest("GET", "https://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + var token string + s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + token = Token(r) + })) - rr := httptest.NewRecorder() - p.ServeHTTP(rr, r) + // Obtain a CSRF cookie via a GET request. + r := createRequest("GET", "/", true) - // POST the token back in the header. - r, err = http.NewRequest("POST", "https://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + rr := httptest.NewRecorder() + p.ServeHTTP(rr, r) - setCookie(rr, r) - r.Header.Set("X-CSRF-Token", token) + // POST the token back in the header. + r = createRequest("POST", "/", true) - // Set a non-matching Referer header. - r.Header.Set("Referer", "http://golang.org/") + setCookie(rr, r) + r.Header.Set("X-CSRF-Token", token) - rr = httptest.NewRecorder() - p.ServeHTTP(rr, r) + // Set a non-matching Referer header. + r.Header.Set("Referer", "https://golang.org/") - if item.shouldPass { - if rr.Code != http.StatusOK { - t.Fatalf("middleware failed to pass to the next handler: got %v want %v", - rr.Code, http.StatusOK) - } - } else { - if rr.Code != http.StatusForbidden { - t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v", - rr.Code, http.StatusForbidden) + rr = httptest.NewRecorder() + p.ServeHTTP(rr, r) + + if item.shouldPass { + if rr.Code != http.StatusOK { + t.Fatalf("middleware failed to pass to the next handler: got %v want %v", + rr.Code, http.StatusOK) + } + } else { + if rr.Code != http.StatusForbidden { + t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v", + rr.Code, http.StatusForbidden) + } } - } + }) } } @@ -347,23 +310,16 @@ func TestWithReferer(t *testing.T) { })) // Obtain a CSRF cookie via a GET request. - r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } - + r := createRequest("GET", "/", true) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. - r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r = createRequest("POST", "/", true) setCookie(rr, r) r.Header.Set("X-CSRF-Token", token) - r.Header.Set("Referer", "http://www.gorillatoolkit.org/") + r.Header.Set("Referer", "https://www.gorillatoolkit.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -387,26 +343,19 @@ func TestNoTokenProvided(t *testing.T) { s.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { token = Token(r) })) - // Obtain a CSRF cookie via a GET request. - r, err := http.NewRequest("GET", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", true) rr := httptest.NewRecorder() p.ServeHTTP(rr, r) // POST the token back in the header. - r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", nil) - if err != nil { - t.Fatal(err) - } + r = createRequest("POST", "/", true) setCookie(rr, r) // By accident we use the wrong header name for the token... r.Header.Set("X-CSRF-nekot", token) - r.Header.Set("Referer", "http://www.gorillatoolkit.org/") + r.Header.Set("Referer", "https://www.gorillatoolkit.org/") rr = httptest.NewRecorder() p.ServeHTTP(rr, r) @@ -419,3 +368,177 @@ func TestNoTokenProvided(t *testing.T) { func setCookie(rr *httptest.ResponseRecorder, r *http.Request) { r.Header.Set("Cookie", rr.Header().Get("Set-Cookie")) } + +func TestProtectScenarios(t *testing.T) { + tests := []struct { + name string + safeMethod bool + originUntrusted bool + originHTTP bool + originTrusted bool + secureRequest bool + refererTrusted bool + refererUntrusted bool + refererHTTPDowngrade bool + refererRelative bool + tokenValid bool + tokenInvalid bool + want bool + }{ + { + name: "safe method pass", + safeMethod: true, + want: true, + }, + { + name: "cleartext POST with trusted origin & valid token pass", + originHTTP: true, + tokenValid: true, + want: true, + }, + { + name: "cleartext POST with untrusted origin reject", + originUntrusted: true, + tokenValid: true, + }, + { + name: "cleartext POST with HTTP origin & invalid token reject", + originHTTP: true, + }, + { + name: "cleartext POST without origin with valid token pass", + tokenValid: true, + want: true, + }, + { + name: "cleartext POST without origin with invalid token reject", + }, + { + name: "TLS POST with HTTP origin & no referer & valid token reject", + tokenValid: true, + secureRequest: true, + originHTTP: true, + }, + { + name: "TLS POST without origin and without referer reject", + secureRequest: true, + tokenValid: true, + }, + { + name: "TLS POST without origin with untrusted referer reject", + secureRequest: true, + refererUntrusted: true, + tokenValid: true, + }, + { + name: "TLS POST without origin with trusted referer & valid token pass", + secureRequest: true, + refererTrusted: true, + tokenValid: true, + want: true, + }, + { + name: "TLS POST without origin from _cleartext_ same domain referer with valid token reject", + secureRequest: true, + refererHTTPDowngrade: true, + tokenValid: true, + }, + { + name: "TLS POST without origin from relative referer with valid token pass", + secureRequest: true, + refererRelative: true, + tokenValid: true, + want: true, + }, + { + name: "TLS POST without origin from relative referer with invalid token reject", + secureRequest: true, + refererRelative: true, + tokenInvalid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var token string + var flag bool + mux := http.NewServeMux() + mux.Handle("/", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + token = Token(r) + })) + mux.Handle("/submit", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + flag = true + })) + p := Protect(testKey)(mux) + + // Obtain a CSRF cookie via a GET request. + r := createRequest("GET", "/", tt.secureRequest) + rr := httptest.NewRecorder() + p.ServeHTTP(rr, r) + + r = createRequest("POST", "/submit", tt.secureRequest) + if tt.safeMethod { + r = createRequest("GET", "/submit", tt.secureRequest) + } + + // Set the Origin header + switch { + case tt.originUntrusted: + r.Header.Set("Origin", "http://www.untrusted-origin.org") + case tt.originTrusted: + r.Header.Set("Origin", "https://www.gorillatoolkit.org") + case tt.originHTTP: + r.Header.Set("Origin", "http://www.gorillatoolkit.org") + } + + // Set the Referer header + switch { + case tt.refererTrusted: + p = Protect(testKey, TrustedOrigins([]string{"external-trusted-origin.test"}))(mux) + r.Header.Set("Referer", "https://external-trusted-origin.test/foobar") + case tt.refererUntrusted: + r.Header.Set("Referer", "http://www.invalid-referer.org") + case tt.refererHTTPDowngrade: + r.Header.Set("Referer", "http://www.gorillatoolkit.org/foobar") + case tt.refererRelative: + r.Header.Set("Referer", "/foobar") + } + + // Set the CSRF token & associated cookie + switch { + case tt.tokenInvalid: + setCookie(rr, r) + r.Header.Set("X-CSRF-Token", "this-is-an-invalid-token") + case tt.tokenValid: + 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 to the next handler: got %v want %v", + rr.Code, http.StatusOK) + } + + if tt.want && !flag { + t.Fatalf("middleware failed to pass to the next handler: got %v want %v", + flag, true) + + } + if !tt.want && flag { + t.Fatalf("middleware failed to reject the request: got %v want %v", flag, false) + } + }) + } +} + +func createRequest(method, path string, useTLS bool) *http.Request { + r := httptest.NewRequest(method, path, nil) + r.Host = "www.gorillatoolkit.org" + if !useTLS { + return PlaintextHTTPRequest(r) + } + return r +}
helpers_test.go+8 −16 modified@@ -83,10 +83,7 @@ func TestMultipartFormToken(t *testing.T) { } })) - r, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", true) rr := httptest.NewRecorder() p := Protect(testKey)(s) @@ -107,13 +104,13 @@ func TestMultipartFormToken(t *testing.T) { mp.Close() - r, err = http.NewRequest("POST", "http://www.gorillatoolkit.org/", &b) - if err != nil { - t.Fatal(err) - } + r = httptest.NewRequest("POST", "/", &b) + r.Host = "www.gorillatoolkit.org" // Add the multipart header. r.Header.Set("Content-Type", mp.FormDataContentType()) + // Add Origin to pass the same-origin check. + r.Header.Set("Origin", "https://www.gorillatoolkit.org") // Send back the issued cookie. setCookie(rr, r) @@ -246,10 +243,8 @@ func TestTemplateField(t *testing.T) { })) testFieldName := "custom_field_name" - r, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("GET", "/", false) + // r, err := http.NewRequest("GET", "/", nil) rr := httptest.NewRecorder() p := Protect(testKey, FieldName(testFieldName))(s) @@ -299,10 +294,7 @@ func TestUnsafeSkipCSRFCheck(t *testing.T) { w.WriteHeader(teapot) })) - r, err := http.NewRequest("POST", "/", nil) - if err != nil { - t.Fatal(err) - } + r := createRequest("POST", "/", false) // Must be used prior to the CSRF handler being invoked. p := skipCheck(Protect(testKey)(s))
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-rq77-p4h8-4crwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-24358ghsaADVISORY
- github.com/gorilla/csrf/commit/9dd6af1f6d30fc79fb0d972394deebdabad6b5ebnvdWEB
- github.com/gorilla/csrf/security/advisories/GHSA-rq77-p4h8-4crwnvdWEB
- lists.debian.org/debian-lts-announce/2025/05/msg00002.htmlnvdWEB
- pkg.go.dev/vuln/GO-2025-3607ghsaWEB
News mentions
0No linked articles in our index yet.