Configuring CORS
for Go (Golang)

tim-armstrong-headshot

Tim Armstrong|June 23, 2021

Configuring CORS headers correctly in Go is easy. This tutorial shows you how.

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. In this particular case, we'll be implementing it for a Golang web app. However, this technique works for any Go HTTP Router framework that supports `http.Handler.` We'll be using Gorilla/Handlers for this tutorial, so we'll be sticking to the Gorilla family and using examples based on Gorilla/Mux.

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.

Find and Fix Application Security Vulnerabilities with Automated Testing

What is CORS?

Configuring CORS for Go (Golang) image

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. 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 Django approach to this, so if Python is your language, then that's definitely worth a read.

In this tutorial, we're looking at a solution for Go.

The Pedagogical Resource

For the rest of this tutorial, we'll be extending the following Gorilla/Mux based server stub:

go
func newREST() *mux.Router {
	r := mux.NewRouter()
	r.HandleFunc("/document", restCreateDocument).Methods("POST")
	return r
}

func main() {
	router := newREST()
	log.Fatal(http.ListenAndServe(":5000", router))
}

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 `restCreateDocument` 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

In order to get started with CORS support, we're going to make use of the `github.com/gorilla/handlers` library as it provides a very simple interface. To do this we're going to extend our `main()` function as follows:

go
func main() {
   router := newREST()
   credentials := handlers.AllowCredentials()
   methods := handlers.AllowedMethods([]string{"POST"})
   ttl := handlers.MaxAge(3600)
   origins := handlers.AllowedOrigins([]string{"www.example.com"})
   log.Fatal(http.ListenAndServe(":5000", handlers.CORS(credentials, methods, origins)(router)))
}

So, let's break down what's going on here.

Credentials

First, we've instantiated the option for allowing our Credentials (Cookies) through:

go
credentials := handlers.AllowCredentials()

This is probably the simplest option as it simply adds the `Access-Control-Allow-Credentials: true` header to the HTTP response.

Access-Control-Allowed-Methods

The second option we've instantiated, `AllowedMethods`

go
methods := handlers.AllowedMethods([]string{"POST"})

validates that the incoming `Access-Control-Request-Method` header's value is within the list of supported methods provided to it during initialization.

Access-Control-Max-Age

The third option `MaxAge`, sets the TTL (in seconds) for the browser to cache an Options response for.

go
   ttl := handlers.MaxAge(3600)

Access-Control-Allowed-Origins

The final option that we've added `Allowed-Origins`

go
origins := handlers.AllowedOrigins([]string{"https://www.example.com"})

validates that the incoming `Origin` header matches one of the origins strings provided to it during initialisation (NOTE: This is protocol dependent, meaning that if your script is coming from https://www.example.com then you need to list that. Omitting the protocol will result in a rejection).

ListenAndServe

Lastly, we modified our `http.ListenAndServe` call by wrapping the `router` object with our middleware engine `handlers.CORS`:

`go
handlers.CORS(credentials, methods, origins)(router)

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 Gorilla/Handlers framework is pretty straightforward, so bending a fork to our will is not too difficult), but in this particular case the library developers are one step ahead of us and have provided a dynamic Option `AllowedOriginValidator(fn OriginValidator).`

To use this option we simply write our own function that matches the signature:

go
type OriginValidator func(string) bool

Which might look something like this:

go
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{"www.example.com"})` option with the new validator.

go
origins := handlers.AllowedOriginValidator(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.

Caveats and Summary

The biggest caveat with using Gorilla/Handlers is that a missing option definition is the same as setting it to a `*` wildcard, which means if you forget to include your origin option in the `handlers.CORS` wrapper then you just opened yourself up to a world of pain.

If there is one thing to criticize of this otherwise incredibly well-designed library, it is this default open design decision made by the library's developers. This design alone is one that is likely to result in many accidental misconfigurations. If they had gone for a default closed design this library would probably be one of the best implementations of a CORS middleware that exists.

Other fantastic libraries from the Gorilla Web Toolkit crew include their CSRF, Sessions, and Schema Middlewares. Each of which makes developing secure Web Apps and API services simpler. So if you consider yourself a security gopher, you should definitely check them out.

Ready to Test Your App

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.

This post was written by Tim Armstrong. Tim has worn many hats over the years, from "Dark Lord of Network Operations" at Nerdalize to "Lead Software Engineer" at De Beers. These days, he's going by "Consultant Engineer & Technical Writer." You can find him on Twitter as @omatachyru, and at plaintextnerds.com.


Tim Armstrong  |  June 23, 2021