TL;DR

I’ve just released jub0bs/cors, a new CORS middleware library for Go, perhaps the best one yet. It has some advantages over the more popular rs/cors library, including

Here is a representative example of client code:

package main

import (
  "io"
  "log"
  "net/http"

  "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // no CORS on this

  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins:        []string{"https://example.com"},
    Methods:        []string{http.MethodGet, http.MethodPost},
    RequestHeaders: []string{"Authorization"},
  })
  if err != nil {
    log.Fatal(err)
  }
  corsMw.SetDebug(true) // optional: turn debug mode on

  api := http.NewServeMux()
  api.HandleFunc("GET /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api)))

  log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, World!")
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

If you’re already convinced and wish to migrate your code to jub0bs/cors without further ado, skip to the migration guide further down this post.

Why you should prefer jub0bs/cors

rs/cors deserves credit for being the most popular CORS middleware library for Go. Its development, still ongoing, spans close to ten years and, to this day, many open-source projects depend on it. But is it perfect? No library is, of course, but I believe Go developers deserve the best. In my opinion, rs/cors suffers from some shortcomings that jub0bs/cors addresses; allow me to detail just a few of them.

Simpler API

If you consult the documentation of rs/cors, you’ll quickly realise that the library provides no fewer than four ways of specifying which Web origins should be allowed by the desired CORS middleware:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Options struct {
  AllowedOrigins []string
  AllowOriginFunc func(string) bool
  AllowOriginRequestFunc func(*http.Request, string) bool /* deprecated */
  AllowOriginVaryRequestFunc func(*http.Request, string) (bool, []string)
  AllowedMethods []string
  AllowedHeaders []string
  ExposedHeaders []string
  MaxAge int
  AllowCredentials bool
  AllowPrivateNetwork bool
  OptionsPassthrough bool
  OptionsSuccessStatus int
  Debug bool
  Logger Logger
}

Not only is such a proliferation of redundant options overwhelming but, as mentioned in an earlier post, some of them are deceptively easy to misuse. In comparison, jub0bs/cors provides a single way of configuring any specific aspect of your desired CORS middleware:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Config struct {
  Origins         []string
  Credentialed    bool
  Methods         []string
  RequestHeaders  []string
  MaxAgeInSeconds int
  ResponseHeaders []string
  ExtraConfig
  // contains filtered or unexported fields
}

More esoteric options are hidden away in a separate struct type named ExtraConfig. Therefore, jub0bs/cors’s API is easier to apprehend and lends itself better to autocomplete.


As a bonus, and contrary to rs/cors, jub0bs/cors allows you to marshal/unmarshal your whole CORS configuration to/from JSON or YAML.


Better documentation

I’ve taken special care to write precise and useful documentation for jub0bs/cors. In particular, the recent addition of enhanced routing patterns in net/http deserved clarification; proper application of a CORS middleware (regardless of which library produced it) in conjunction with the use of “method-full” patterns can indeed be challenging at first. I myself certainly was confused until Carlana Johnson helped me realise that http.ServeMux composition is key. To spare users of jub0bs/cors similar confusion, I’ve included elucidating examples in the documentation.

On top of that, although jub0bs/cors plays best with net/http’s router, I’ve released examples involving third-party routers (such as Chi, Echo, and Fiber) in a separate GitHub repository.

Extensive configuration validation

In an earlier blog post and in the talk I gave at GopherCon Europe 2023, I argued that the lack of configuration validation is one of the main reasons why most people struggle to troubleshoot CORS errors. Unfortunately, a year later, rs/cors has not improved in this respect; consider the following code sample:

import "github.com/rs/cors"

func main() {
  corsMw := cors.New(cors.Options{
    AllowedOrigins: []string{
      "https://example.org",
      "https://*.com",
    },
    AllowedMethods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    AllowedHeaders: []string{"auth orization"},
  })
  // rest omitted for brevity
}

Can you spot issues with the CORS configuration of the desired middleware? Perhaps you can (with some scrutiny) but rs/cors itself cannot, simply because it performs almost no validation of your CORS configuration. Instead, it’s quite content to produce either a dysfunctional middleware (which will likely cause you much frustration) or an insecure one (which will put your users at risk).

Unlike rs/cors, jub0bs/cors performs extensive configuration validation in a bid to prevent you from creating dysfunctional or insecure CORS middleware:

import (
  "fmt"
  "os"

  "github.com/jub0bs/cors"
)

func main() {
  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins: []string{
      "https://example.org",
      "https://*.com",
    },
    Methods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    RequestHeaders: []string{"auth orization"},
  })
  if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
  // rest omitted for brevity
}

The programme above fails (as it should) with an error message that alerts you to all the issues with your CORS configuration:

cors: forbidden method name "CONNECT"
cors: invalid method name "RÉSUMÉ"
cors: invalid request-header name "auth orization"
cors: for security reasons, origin patterns like "https://*.com" that
  encompass subdomains of a public suffix are by default prohibited

Debug mode

Most CORS middleware libraries tend to omit all CORS headers in responses to failed preflight requests. rs/cors behaves like this; and jub0bs/cors does too, at least by default.

On the one hand, this behaviour adheres to good security practice: CORS middleware shoud ideally reveal as little as possible about their configuration (such as allowed origins, allowed methods, etc.) to potential adversaries when preflight fails. On the other hand, and as explained in an earlier post, this behaviour severely impedes troubleshooting of CORS issues: the browser, left with insufficient information about the preflight failure, ends up raising an error whose message masks the root cause of your CORS issue.


Typically, you get an error message like the following:

Access to fetch at https://your-server.example.com/users from origin https://your-client.example.com has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No Access-Control-Allow-Origin header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to no-cors to fetch the resource with CORS disabled.

Perplexed, you go and double-check your server’s CORS configuration, and you find that https://your-client.example.com is in fact listed as an allowed origin there… 🤔

Finally, after hours getting nowhere, you discover the root cause of your CORS issue: the server’s configuration is insufficiently permissive because the client’s requests include some header (Authorization, say) that happens not to be explicitly allowed, but should be. 🤬


In my opinion, this behaviour of CORS middleware is one of the main reasons why CORS errors have a notorious reputation of being difficult and time-consuming to troubleshoot.

rs/cors gets out of the difficulty by letting its users specify a logger as part of their middleware configuration. That logger then emits an informative message for every request handled by the middleware:

2024/04/23 13:40:12 Handler: Preflight request
2024/04/23 13:40:12   Preflight aborted: headers '[Authorization]' not allowed
2024/04/23 13:40:13 Handler: Preflight request
2024/04/23 13:40:13   Preflight aborted: method 'PUT' not allowed
2024/04/23 13:40:14 Handler: Preflight request
2024/04/23 13:40:14   Preflight aborted: origin 'https://example.com' not allowed
2024/04/23 13:40:15 Handler: Actual request
2024/04/23 13:40:15   Actual request no headers added: missing origin
2024/04/23 13:40:17 Handler: Actual request
2024/04/23 13:40:17   Actual request no headers added: missing origin
2024/04/23 13:40:18 Handler: Actual request
2024/04/23 13:40:18   Actual response added headers: map[Access-Control-Allow-Origin:[https://example.org] Vary:[Origin]]

This approach does ease troubleshooting, but is far from ideal: you can imagine how much noise such a logger generates on a CORS-aware server under heavy load… 🤢

jub0bs/cors takes a different approach: its CORS middleware provides a debug mode, which you can toggle via the (*Middleware).SetDebug method:

1
2
3
4
5
6
// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{ /* omitted */ }
if err != nil {
  log.Fatal(err)
}
corsMw.SetDebug(true)

The debug mode, when switched on, overrides the middleware’s behaviour described above and includes more information in responses to preflight requests, even failed ones. Switching debug mode on essentially turns your CORS middleware into a “browser whisperer”: by giving the browser just enough contextual information, the middleware is able to elicit error messages from it that you will actually find helpful for resolving your CORS issues. Therefore, I strongly encourage you to activate this debug mode whenever you’re facing a puzzling CORS issue.

But wait; there’s more! Because the SetDebug method is concurrency-safe, you’re free to opportunistically toggle debug mode on the fly, even as your server is running and your CORS middleware is processing requests (i.e. without the need to stop the server, edit its source code, and then restart the server). All you have to do is somehow expose the ability to toggle debug mode; as an example, I’ve modified the programme at the top of this post by adding a /debug endpoint for toggling debug mode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
    "io"
    "log"
    "net/http"
    "strconv"

    "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // no CORS on this

  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins:        []string{"https://example.com"},
    Methods:        []string{http.MethodGet, http.MethodPost},
    RequestHeaders: []string{"Authorization"},
  })
  if err != nil {
    log.Fatal(err)
  }

  setDebug := func(w http.ResponseWriter, r *http.Request) {
    debug, err := strconv.ParseBool(r.URL.Query().Get("debug"))
    if err != nil {
      http.Error(w, "invalid debug value", http.StatusBadRequest)
      return
    }
    corsMw.SetDebug(debug)
  }
  mux.Handle("PUT /debug", authZMiddleware(http.HandlerFunc(setDebug)))

  api := http.NewServeMux()
  api.HandleFunc("GET /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api)))

  log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, World!")
}

func authZMiddleware(h http.Handler) http.Handler {
  return nil // actual implementation omitted
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
  // omitted
}

Note that, in practice, just like you shouldn’t publicly expose your pprof endpoints, you shouldn’t publicly expose the ability to toggle this debug mode to the entire world. Accordingly, in the example above, I’ve wrapped my setDebug handler in some authorization middleware. An alternative approach would consist in restricting access to the /debug endpoint at the reverse-proxy level.


Attentive readers of my Fearless CORS design philosophy may object that jub0bs/cors’s debug mode seems to violate principle 11:

Guarantee configuration immutability.

But I would retort that switching the debug mode on only slightly alters the middleware’s behaviour to ease troubleshooting; toggling the debug mode doesn’t modify the sets of allowed origins, allowed methods, allowed request headers, max age, etc. Such configuration modification still require a server restart. 😇


Stronger performance guarantees

Overall, jub0bs/cors and modern version of rs/cors have similar performance characteristics. However, there’s one specific situation in which rs/cors v1.10.1 fares badly, to the point of facilitating denial of service. In response to some malicious requests masquerading as CORS-preflight requests, rs/cors middleware indeed allocate an inordinate amount of memory, orders of magnitude more than jub0bs/cors middleware in some cases:

goos: darwin
goarch: amd64
pkg: github.com/jub0bs/cors-benchmarks
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
                │    rs-cors    │              jub0bs-cors  │
                │    sec/op     │   sec/op     vs base      │
malicious_ACRH    17238.0n ± 3%   438.2n ± 5%  -97.46% 😱

                │   rs-cors     │              jub0bs-cors  │
                │     B/op      │     B/op     vs base      │
malicious_ACRH    37832.0 ± 0%     928.0 ± 0%  -97.55% 😱

In the worst (yet realistic) case I could conjure up, a single malicious request of 1 Mib causes rs/cors middleware to allocate a gargantuan 116 MiB!

This behaviour can be abused by adversaries to produce undue load on the server’s runtime (memory allocator and garbage collector). Of course, this attack vector isn’t as severe as, say, ReDoS, and most WAFs would likely block those malicious requests, but it should still be cause for concern. In particular, because CORS middleware typically must sit in front of any authentication logic, attackers don’t even need to be authenticated.

After discovering this problem in rs/cors v1.10.1, I promptly opened GitHub issue #170 and sent a fix in pull request #171. My pull request was eventually merged and a new version (v1.11.0) was released, but only about a month after I filed the issue. Regardless of the vulnerability’s actual severity, the maintainer’s tolerance to leaving the issue unresolved for so long is worrying. 😟

Many open-source projects that depend on rs/cors v1.10.1 (or even older versions) could suffer from issue #170. One of them, Prometheus Alertmanager, is advertised as a programme whose normal operation requires no more than 50 Mib of memory. To assess impact, I conducted a test in which I concurrently sent a couple of malicious requests to a Dockerised instance of Alertmanager running with a memory limit of 50 Mib; as a result, the Docker container quickly ran out of memory and died. 💀

Because jub0bs/cors follows a defensive approach, it is immune to such issues and exhibits predictable performance characteristics.

Reasons for favouring rs/cors over jub0bs/cors

Despite all of jub0bs/cors’s goodness, you may still have valid reasons for sticking with rs/cors v1.11.0+, at least for the time being. Here is as exhaustive a list as I could come up with:

  • You cannot yet, for some reason, migrate to Go v1.22 (whose semantics are assumed by jub0bs/cors).
  • You wish to allow Web origins whose scheme is neither http nor https.
  • You need more flexible origin patterns than those supported by jub0bs/cors.
  • You need to modify your CORS middleware’s configuration on the fly, without restarting the server.
  • You want to log an event for every single request processed by your CORS middleware.

If none of those items describe your present situation, I encourage you to migrate to jub0bs/cors as soon as possible. 😇

Migration guide

If you’re ready to migrate from rs/cors to jub0bs/cors, I expect that you will find such migration straightforward. The following subsections of this post highlight the similarities and differences between the two libraries. If you still struggle to migrate your project, feel free to ask me (on Mastodon) for guidance or even a pull request.

In all of the examples below where a variable named handler appears, the variable is assumed to be of type http.Handler and declared elsewhere.

Installing jub0bs/cors

To start depending on jub0bs/cors simply run the following shell command within your project:

go get github.com/jub0bs/cors

Once you directly depend on jub0bs/cors and no longer on rs/cors, don’t forget to tidy your module by running fhe following command:

go mod tidy

Configuring a CORS middleware

The basic configuration struct types have different names: rs/cors’s is Options, whereas jub0bs/cors’s is Config. The names of those struct types’ fields also differ; the table below shows the mapping between corresponding fields in the two libraries:

rs/cors jub0bs/cors
AllowedOrigins Origins
AllowOriginFunc N/A
AllowOriginRequestFunc N/A
AllowOriginVaryRequestFunc N/A
AllowedMethods Methods
AllowedHeaders RequestHeaders
ExposedHeaders ResponseHeaders
MaxAge MaxAgeInSeconds
AllowedCredentials Credentialed
AllowPrivateNetwork ExtraConfig.PrivateNetworkAccess
OptionsPassthrough N/A
OptionsSuccessStatus ExtraConfig.PreflightSuccessStatus
Debug N/A (but see debug mode)
Logger N/A

Moreover, jub0bs/cors’s ExtraConfig struct type allows you to unlock additional options not mentioned in the table above.

Creating a middleware and applying it to a handler

The functions that produce CORS middleware also have different names: rs/cors’s is named New, whereas jub0bs/cors’s is named NewMiddleware.

Besides, jub0bs/cors’s NewMiddleware returns, not only a CORS middleware, but also an error. Do inspect that error result; only if its value is nil can you assume that the resulting middleware is usable.

Finally, whereas the method for applying a middleware to a http.Handler is (confusingly) named Handler in rs/cors, the equivalent method is named Wrap in jub0bs/cors.

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{ /* omitted */ })
if err != nil {
  // corsMw is unusable; bail out.
  log.Fatal(err)
}
handler = corsMw.Wrap(handler) // wrap corsMw around handler.

Default configurations

In contrast to rs/cors, and for good reasons explained elsewhere, jub0bs/cors provides no default CORS configurations. If the code to migrate relies on rs/cors’s Default or AllowAll functions, the code snippets below illustrate how to adapt your code for migrating to jub0bs/cors:

// rs/cors
handler = cors.Default().Handler(handler)

is equivalent to

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{
  Origins: []string{"*"},
  Methods: []string{
    http.MethodGet,
    http.MethodHead,
    http.MethodPost,
  },
  RequestHeaders: []string{
    "Accept",
    "Content-Type",
    "X-Requested-With",
  },
})
if err != nil {
  // omitted: bail out, somehow
}
handler = corsMw.Wrap(handler)

and

// rs/cors
handler = cors.AllowAll().Handler(handler)

is equivalent to

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{
  Origins: []string{"*"},
  Methods: []string{
    http.MethodHead,
    http.MethodGet,
    http.MethodPost,
    http.MethodPut,
    http.MethodPatch,
    http.MethodDelete,
  },
  RequestHeaders: []string{"*"},
})
if err != nil {
  // omitted: bail out, somehow
}
handler = corsMw.Wrap(handler)

On the genesis of jub0bs/cors

Reflection on its predecessor

About a year ago, after realising that developers’ troubles with Cross-Origin Resource Sharing (CORS) can chiefly be blamed on tools rather than on their users, I released jub0bs/fcors, a reference implementation for Fearless CORS, my design philosophy for CORS middleware libraries,

On a personal note, this original library proved to be a formative laboratory of ideas, not only about library design, but also about algorithms and data structures (especially radix trees).

However, although jub0bs/fcors garnered praise from developers, OWASP, and some WHATWG folks (in private communication), its adoption remains disappointingly limited. For instance, at the time of writing this post, the project’s GitHub repository has only accrued a paltry 79 stargazers; nothing to scoff at, but far from a resounding success. In comparison, rs/cors boasts more than 2,500 stargazers.

If pressed to speculate about the reasons for jub0bs/fcors’s lacklustre adoption, I would first argue that a contending software library, regardless of its merits, is unlikely to quickly rise as high as an incumbent library in popularity. Second, I would cite some controversial design decisions in jub0bs/fcors. That library indeed relies heavily on functional_options, a much maligned pattern in the Go community. Winning over staunch detractors of the pattern was always going to be an uphill battle, and the talk I gave on the topic at GopherCon Europe 2023, though well received, did little to sway them. I myself have somewhat changed my mind about the pattern over the last few months; I still believe it’s useful in some situations, but some of its pain points have become more apparent to me.

Rather than discourage me, jub0bs/fcors’s unspectacular adoption spurred me to write a spiritual successor: a new CORS library for Go, one that adheres to the principles of Fearless CORS but whose more traditional API holds the promise of universal appeal: jub0bs/cors.

Why not contribute to rs/cors instead?

Finally, I should address the elephant in the room: Why produce a competing library? Why not contribute improvements to rs/cors instead? The answer isn’t as straightforward as it may seem.

Although I can find faults in the design of jub0bs/fcors, there is little I would change in my Fearless CORS design philosophy. I remain convinced that its twelve principles are sound and deserve to spread to other CORS libraries (even ones beyond Go’s ecosystem). Armed with this ambition, I did contribute issues and pull requests to rs/cors, most of which Olivier Poitrey, the library’s maintainer, kindly fixed or merged. Moreover, there is little doubt that jub0bs/fcors inspired Olivier to improve aspects of rs/cors’s performance.

However, not being at the helm of rs/cors, I am limited in how much I can influence its development; and the truth is that the library is still a far cry from the ideal conveyed by Fearless CORS, i.e. a CORS library easy to use and hard to misuse. For instance, the multiple hooks (e.g. AllowOriginFunc) that rs/cors provides are, in my opinion, dangerous misfeatures. How to improve this aspect of the library isn’t obvious; in fact, one more such hook recently crept into the API. Deprecating all those hooks would be a good first step, but altogether removing support for them would constitute a breaking change. Perhaps a hypothetical v2 of rs/cors could bring the library in line with Fearless CORS, but whether Olivier plans to release a new major version in the near future is unclear to me.

If I cannot bend rs/cors into a shape close to my ideal of a CORS middleware library, I can at least try to displace it with a better library. This is my ambition with jub0bs/cors.

Acknowledgements

Thanks to Carlana Johnson and Mike Stephen for taking the time to review an early draft of this post.