Cross-site request forgery (CSRF) is a type of attack that occurs when a malicious website tricks a user into submitting a form to another website they are signed into. For instance, if I were signed into my bank’s website and then visited fake-bank.com, it might display a form similar to the one below:
<form action="https://bank.com/transfer" method="post">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="recipient" value="hacker-account">
<input type="submit" value="Contact Support">
</form>
The goal with this form is to have me click the submit button thinking it will get me in touch with support, but in reality it will submit the form to my bank’s website.
This type of attack could potentially work because browsers will automatically send the user’s cookies to the website when the form is submitted. In this case, it might send a cookie with my authenticated bank session, resulting in a transfer being submitted that I didn’t intend.
CSRF protection is a way to detect and prevent this type of attack.
Historically, the most common way to protect against CSRF was for a server to generate a token that would be both stored as a cookie, and added as a hidden input field to every form. The server would then check that the token in the hidden input field matched the token in the cookie when the form was submitted.
This approach works because nefarious websites cannot read the user’s cookies, so they cannot recreate the hidden input field required for the forms.
The primary downsides to this approach are:
Modern browsers now include a number of additional headers, and some of these can be used to help protect against CSRF attacks.
The first is the Sec-Fetch-Site
header. This header will tell us if a request is cross-origin or not. If a request has a value of same-site
or none
for this field, it signals that it is NOT a cross-origin request and we don’t need any CSRF protections. Any other values, or a lack of a value, and we need to assume that it is a cross-origin request and look at the Origin
header to determine how to proceed.
The Origin
header will be set with the origin of the website that initiated the request. For instance, if we are on https://fake-bank.com
, then this will be added to the Origin
header when we click a link or submit a form. We can use this to determine if a request is coming from a trusted origin. For instance, if our API is hosted at https://api.calhoun.io
, but we want to allow cross-site requests from a different subdomain, we could setup our CSRF protection to allow the https://client.calhoun.io
origin, but reject any others that are cross-origin.
If no origin or sec-fetch-site header is present, we assume that the request was not created by a browser, and we do not enforce CSRF protections. This allows things like API client libraries to communicate with servers without CSRF protections causing issues.
The biggest advantage to this approach is that it is much easier to implement than the traditional approach using tokens. There is no need to add a CSRF field to every form, nor do we need a CSRF token cookie.
The major downside to this approach is that it requires users to have a somewhat recent browser. Any browser released after 2023 should have both headers, and the origin header has been around in most browsers for a bit longer, but if you have users running really old browsers you might need to do some digging to verify if this will work for you.
In Go 1.25, the CrossOriginProtection type was added to the net/http
package. This type makes use of the second approach - utilizing the Sec-Fetch-Site
and Origin
headers to provide CSRF protection.
Below is an example of the CrossOriginProtection
type being used to protect a website.
mux := http.NewServeMux()
mux.HandleFunc("/", homePageHandler)
mux.HandleFunc("POST /login", loginHandler)
// more handlers...
csrfProtection := http.NewCrossOriginProtection()
// Allow cross-site requests from the client subdomain
csrfProtection.AddTrustedOrigin("https://client.calhoun.io")
// Wrap the handler with the CSRF Protection
handler := csrfProtection.Handler(mux)
// Serve the handler on port 8080
http.ListenAndServe(":8080", handler)
}
This code will allow any same-origin requests, as well as cross-site requests from the https://client.calhoun.io
origin. We can verify this by running some curl commands and attaching various headers.
First, let’s try a valid request that is same-origin.
curl -v -X POST \
-H "sec-fetch-site:same-origin" \
localhost:8080/login
Running this we will see a 200 OK response, as it is coming from the same origin.
If we were to instead send a request from the same site, but from a different origin, we will see an error unless it is an origin that we have explicitly allowed via the AddTrustedOrigin
method.
# This works
curl -v -X POST \
-H "sec-fetch-site:same-site" \
-H "origin:https://client.calhoun.io" \
localhost:8080/login
# This will error because the origin is not trusted
curl -v -X POST \
-H "sec-fetch-site:same-site" \
-H "origin:https://bob.calhoun.io" \
localhost:8080/login
Similarly, we can recreate what happens if we only use the Origin
header like older browsers, and even what happens if we don’t have any headers at all like a non-browser client.
# This works by comparing the origin to both
# the host and trusted origins.
# This could also be set to http://localhost:8080
curl -v -X POST \
-H "origin:https://client.calhoun.io" \
localhost:8080/login
# This works because the CSRF protection assumes it
# is a non-browser request with no headers.
curl -v -X POST \
localhost:8080/login
One downside I have noticed with the CrossOriginProtection type is that it doesn’t appear to support wildcards in the trusted domains. That is, we can’t add a trusted origin like https://*.calhoun.io
to allow all subdomains of calhoun.io
.
There is an AddInsecureBypassPattern method, but this appears to be used more for specifying paths where CSRF protections should be disabled. For instance, if we want to allow OAuth requests to be made from any origin, we can add a bypass pattern like /oauth
.
csrfProtection.AddInsecureBypassPattern("/oauth")
This will allow requests to the /oauth
path to be made from any origin, without CSRF protections.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.