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 from
  • sec-fetch-site:same-origin if the request goes to the site listed in origin:
  • sec-fetch-site:cross-site if the request goes to a different site than the one listed in origin:

…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 says cross-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:


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.