CORS in Go (Golang) using rs/cors
In previous articles, we’ve covered what CORS is and the reverse proxy methods to Fixing “no ‘access-control-allow-origin’ header present”, but what do you do when you need a bit more flexibility?
This article guides you through the process of handling CORS in the backend.
This technique works for any Go HTTP Router framework that supports http.Handler
.
We’ll be using rs/cors
for this tutorial as this is compatible with pretty much every HTTP Router Framework.
Naturally, as with programming in general, there are multiple ways of implementing the same thing, from a pure DIY approach to something prebuilt and packed nicely in a library. As indicated above, in this tutorial, we’ll be following the latter approach. But, before we get started with all that, let’s take a quick ReCap of the problem we’re trying to address.
What is CORS?
Cross-Origin Resource Sharing (CORS) is a protocol for relaxing the Same-Origin policy to allow scripts from one [sub]domain (Origin) to access resources at another. It does this via a preflight exchange of headers with the target resource.
When a script makes a request to a different [sub]domain than it originated from the browser first sends an OPTIONS
request to that resource to validate that the resource is expecting requests from external code.
The OPTIONS request carries the Origin
header, along with some other information about the request (check out the CORS explainer for more detail). The target resource then validates these details and (if valid) responds with its own set of headers describing what is permissible and how long to cache the preflight response for.
In our Fixing “no ‘access-control-allow-origin’ header present” article we did this validation and response generation with an Nginx Reverse-proxy and some RegEx. Which, while a good DevOps solution to the problem, lacks a degree of flexibility and relies heavily on our RegEx being safe. This Reverse-Proxy approach we covered in that article is a very good stopgap solution as it is easy to set up and requires no code changes. It does however have some significant shortcomings.
The biggest of which being the RegEx at the centre of that approach.
Risks of RegEx
A large number of CORS vulnerabilities are caused by misconfigured RegEx search strings in such reverse-proxy configurations.
For example, the RegEx string ^https\:\/\/.*example\.com$
might at first glance look like a valid solution to allowing us to have scripts from any subdomain of example.com contact our API over HTTPS. It certainly does work for such cases, as both www.example.com
and blog.example.com
would satisfy this RegEx.
However, what a lot of people miss is that it also accepts literally anything that starts with https://
ends in example.com
. So, for example, even www.evilexample.com
is a perfectly valid origin then according to the RegEx search string.
Obviously then, we want a solution that is similarly flexible (if not more flexible) while carrying a lower risk of misconfiguration.
Code-based solutions
Our next port of call then is to start looking at implementing this with as minimal a code change as possible.
Fortunately, every production-ready web server framework has some level of CORS support. In an earlier article, we covered a Python/FastAPI approach to this.
The pedagogical resource
For the rest of this tutorial, we’ll be extending the following nte/http based server stub:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /document", documentHandler)
http.ListenAndServe(":8080", mux)
}
Here we’ll assume that the resource is hosted at api.example.com and the request is coming from www.example.com. The route of https://api.example.com/document
accepts a POST
request where it expects a JSON blob (the document) and some authentication Cookie.
To keep things simple we’ll assume that there is some handler documentHandler
that appropriately validates the cookie and accepts the document. In reality, we’d want a separate middleware to handle the cookie validation so that we can be sure that all protected endpoints are processed properly, but we’ll gloss over that for this tutorial.
Adding basic CORS support
Adding CORS support using rs/cors
is very easy.
To do this, the first thing you need to do is add "github.com/rs/cors"
to your import.
Next, we’re going to extend our main()
function as follows:
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://www.example.com"},
AllowedMethods: []string{"POST"},
AllowCredentials: true,
MaxAge: 3600,
})
mux := http.NewServeMux()
mux.HandleFunc("POST /document", documentHandler)
http.ListenAndServe(":8080", c.Handler(mux))
}
So, let’s break down what’s going on here.
ListenAndServe
Lastly, we modified our http.ListenAndServe
call by wrapping the mux
object with our middleware engine c.Handler
:
c.Handler(mux)
This is what does the heavy lifting here, intercepting our incoming queries to validate any preflight OPTIONS requests and validate the CORS headers before passing the request to our router.
Going further
So far we’ve replicated a similar result to our Nginx Reverse-Proxy solution, which is fine, but we’re working in code for a reason. We wanted to be able to dynamically add new Origins to our list of supported Access-Control-Allow-Origin
responses (using a database perhaps).
So how do we make it dynamic?
Well, we could write our own wrapper function from scratch (the rs/cors
framework is pretty straightforward, so bending a fork to our will is not too difficult), but in this case the library developers are one step ahead of us and have provided a dynamic Option AllowedOriginFunc(fn OriginValidator)
.
To use this option we simply write our own function that matches the signature:
type OriginValidator func(string) bool
Which might look something like this:
func originValidator(origin string) bool {
valid := false
err := pool.QueryRow("SELECT IF(origin=?, True, False) as 'valid' FROM origins", origin).Scan(&valid)
if err != nil { return false }
return valid
}
Here, our example originValidator
uses SQL to compare the incoming origin against our database and if it’s found then it returns true
if it’s not found then the function will return false
.
The next thing we need to do is to simply replace our AllowedOrigins: []string{"https://www.example.com"},
option with the new validator.
AllowedOriginFunc: originValidator,
How does this work then?
Behind the scenes, the main wrapper uses our function to validate if the Origin header is acceptable. Then, if it passes it, the wrapper will reflect the received origin into the access-control-allow-origin
field.
This means it’s really important that our custom originValidator
code is well tested and handles any potential exceptions safely. It is always better in this kind of code to fail-safe, as rejecting a potentially valid request is always preferable to accepting a potentially invalid one.
Further reading
If you hunger for more CORS knowledge check out our “What is CORS?” explainer article, or our Fixing “no ‘access-control-allow-origin’ header present” article.
If you’re looking for tutorials for other languages take a look at these:
Feel free to drop us a message if you’re looking for one that we haven’t covered yet.