Don't Mess With My Site!
Cross-Site Request Forgery (CSRF) is a tricky attack. Imagine you are logged in to your bank at yourbank.com to transfer some money. Something distracts you while the banking page is still open, and by bad luck, you get tricked into visiting evil.com. The page from evil.com contains a malicious script that sends a forged form to yourbank.com through a background POST request.
The problem with forged cross-domain forms
Because you have a banking session open in another tab, the forged from includes the session cookie. (Browsers don't limit cookies to a single tab, to allow users to open multiple tabs inside a session.)
Without a protection in place, the forged form could perform a money transfer from your bank account, without you even noticing (until you check the transaction history).
(The above description might be an oversimplification, but the point is that without CSRF protection, malicious code loaded from domain A can intrude an authenticated session of domain B.)
Browsers provide protection, but…
Modern browsers protect their users against CSRF by including headers in a request:
origin:<site>
contains the site that the page that sent this request was loaded fromsec-fetch-site:same-origin
if the request goes to the site listed inorigin:
sec-fetch-site:cross-site
if the request goes to a different site than the one listed inorigin:
…the web server must do its part
The web server that receives this request can check these headers to determine if the request is legit:
- If the request is a GET, HEAD, or OPTIONS, request, the request is valid. (These methods are read-only if implemented correctly and hence considered safe)
- If the origin is the site that receives the request, the request is valid.
- If the origin is from a different site,
sec-fetch-site
sayscross-site
, and the origin isn't blacklisted on the web server, the request is valid. - In all other cases (
origin
header is missing,sec-fetch-site
is missing), the request is valid (because the request is either same-origin or not a browser request).
Go 1.25 greatly simplifies implementing CSRF protection
How can you make an http.Server
detect CSRF attacks? With Go < 1.25, with carefully handcrafted code. With Go 1.25, the new type http.CrossOriginProtection
has you covered.
Here is how it works (assuming you are the developer in charge of writing yourbank.com's money transfer handler):
You wrote a sophisticated handler like so:
transferHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This code must only run if CSRF protection passes!
fmt.Fprintf(w, "Transfer successful!")
})
To add CSRF protection to the handler, create a new CrossOriginProtection
value and add the origins your handler trusts. For example, if your users can load pages and data from yourbank.com
and mobile.yourbank.com
, add these as trusted origins. Then wrap your handler with the new cross origin protection:
csrf := http.NewCrossOriginProtection()
csrf.AddTrustedOrigin("https://yourbank.com")
csrf.AddTrustedOrigin("https://mobile.yourbank.com")
protectedHandler := csrf.Handler(transferHandler)
Now wire the handler to the desired path (/transfer
in this example) and start the server:
http.Handle("/transfer", protectedHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
Done! Your handler is CSRF-protected.
Try it out: Paste the above Go code into a func main()
add imports, and run it with Go 1.25 RC2 or later. (Find the full code, including package statement and imports, in the Go playground. It won't run there, but it's ready to be run on your machine.)
Once the server is up and running, test the following curl
requests. (Note the --data ""
flag that enforces a POST request.)
# Same-origin request:
curl -X POST -H "sec-fetch-site:same-origin" http://localhost:8080/transfer
# Result: "Transfer successful!"
# cross-site request from a trusted origin (yourbank.com):
curl -X POST -H "origin:https://yourbank.com" -H "sec-fetch-site:cross-site" http://localhost:8080/transfer
# Result: "Transfer successful!"
# Different origin, no sec-fetch-site:
curl -X POST -H "origin:https://evil.com" http://localhost:8080/transfer
# Result: "cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host"
# Browser flags the request as cross-site but sends no origin header:
curl -X POST -H "sec-fetch-site:cross-site" http://localhost:8080/transfer
# Result: "cross-origin request detected from Sec-Fetch-Site header"
# Cross-site request from an untrusted origin:
curl -X POST -H "origin:https://evil.com" -H "sec-fetch-site:cross-site" http://localhost:8080/transfer
# Result: "cross-origin request detected from Sec-Fetch-Site header"
Thwarting cross-site request forgery has become a good deal easier!
Further reading
Find out more about CSRF:
- Cross Site Request Forgery (CSRF) | OWASP Foundation
- Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series
Update 2025-07-20: Clarify that GET, HEAD, and OPTION are safe with regard to modifying data or triggering server-side actions. • Replaced --data ""
by -X POST
because the latter clearly shows that a post call is sent.
Update 2025-07-28: Fix the fourth bullet point in the list of headers to check: If the request contains neither of sec-fetch-site
nor origin
, the request is valid, rather than rejected. Thanks to @[email protected] for pointing out this error.