TL;DR

In this short follow-up to my previous post, I describe why and how I’ve added support for dynamic reconfiguration of CORS middleware in jub0bs/cors.

Rethinking configuration immutability

Up until now, I’ve been arguing that CORS middleware should not be reconfigurable on the fly and that any change to their configuration should require a server restart:

Insofar as CORS relaxes some of the restrictions enforced by the SOP, it is a security-critical mechanism. Accordingly, a server’s CORS configuration should be subject to change control: more specifically, I argue that any update to CORS configuration warrants careful vetting and should be followed by a server restart. […] if developers perceive server startup as prohibitively slow, they should, in my opinion, focus their efforts on reducing startup time rather than on avoiding the need to restart the server.

A middleware built with jub0bs/fcors [my initial reference implementation] is effectively immutable, and any amendment to the corresponding CORS policy requires a server restart. Although this constraint does not guarantee that CORS-policy changes will get reviewed, I believe that it at least nudges developers to adopt such a practice.

I was careful to hedge my bets a bit, though:

Although I’ve endeavoured to avoid past design mistakes of others, I’ve likely made brand new ones myself.

And I’m glad I did. Some recent feedback about jub0bs/cors, my latest reference implementation, has compelled me to rethink this principle and somewhat soften my stance on the topic. I still very much value immutable infrastructure, but this constraint can prove both excessively restrictive and ineffective for my stated goals:

  • It’s excessively restrictive because it impedes snappy zero-downtime updates, at least in some cases; picture, for instance, a multi-tenant SaaS company’s CORS-aware API whose (re-)deployment takes a prohibitively long time.
  • It’s ineffective because, as I hinted at in the passage quoted above, it alone does not guarantee change control and auditability. In retrospect, I must admit that my attempt to address such governance concerns at the library level was perhaps ill-advised.

Many situations warrant a more pragmatic approach, and configuration immutability can advantageously be lifted, as long as concurrency safety is maintained.

jub0bs/cors middleware are now reconfigurable

After gathering my thoughts for a while and eliciting some feedback from the Go community, I was able to retrofit jub0bs/cors for on-the-fly middleware reconfigurability in a way that doesn’t undermine my entire design philosophy, introduce breaking changes, or compromise performance. jub0bs/cors still provides the following function for creating CORS middleware,

func NewMiddleware(cfg Config) (*Middleware, error)

but v0.2.0 saw the addition of two methods:

func (m *Middleware) Reconfigure(cfg *Config) error
func (m *Middleware) Config() *Config

The Reconfigure method, as its name implies, allows you to reconfigure middleware m. If the cfg argument is non-nil but *cfg is invalid, Reconfigure returns some non-nil error and leaves m unchanged. If cfg is nil, Reconfigure disables CORS; it essentially turns m into a “passthrough” middleware, i.e. a middleware that does nothing interesting and merely delegates to the handler(s) it wraps. Be aware that mutating the fields of *cfg after Reconfigure has returned does not alter m’s behaviour.

The Config method returns a pointer to a deep copy of the Config value with which CORS middleware m was built or last reconfigured with. Thanks to this method, you don’t need to keep your middleware’s configuration in scope in anticipation of amending it (e.g. to augment the set of allowed Web origins); you can simply query the configuration when needed. m.Reconfigure(m.Config()) is guaranteed to be a no-op (albeit a relatively expensive one). Again, be aware that mutating the fields of the Config method’s result does not alter m’s behaviour.

Because both methods are concurrency-safe, you can confidently reconfigure a middleware and/or query its current configuration even as it’s concurrently processing requests. Therefore, you are free to somehow expose those methods so you can exercise them without having to restart your server; however, if you do expose those methods, you should only do so on some internal or authorized endpoints, for security reasons.

Moreover, a happy byproduct of refactoring is that the zero value of Middleware is now ready to use, though it merely corresponds to a “passthrough” middleware.

One word of caution, though: be aware that frequent CORS-middleware reconfiguration may exacerbate caching issues; I refer you to the relevant section of Jake Archibald’s blog post about CORS.

Comparison with rs/cors’s hook-based approach

Once again, jub0bs/cors bears comparison with rs/cors, which remains, at the time of writing this post, the most popular CORS library for Go. rs/cors’s flexibility stems from its multiple “hooks”:

type Options struct {
  // ...
  AllowOriginFunc            func(origin string) bool
  AllowOriginRequestFunc     func(r *http.Request, origin string) // deprecated
  AllowOriginVaryRequestFunc func(r *http.Request, origin string) (bool, []string)
  // ...
}

Via those hooks, you can specify strategies for discriminating allowed and disallowed Web origins. However, I believe, for reasons explained in an earlier post, that such hooks are error-prone and too powerful for your own good; I have therefore steadfastly refused to introduce the likes of them in my CORS libraries, and I still do.

Instead, in order to support dynamic middleware reconfigurability in jub0bs/cors, I’ve basically turned rs/cors’s hook-based approach inside out, like you would a sock: rather than specify a callback representing your strategy, you must declaratively describe your desired configuration in the form of a *Config value and pass that value to your middleware’s Reconfigure method. This approach comes with several benefits that cannot be understated:

  • Flexibility: you can update, not just the set of allowed origins, but the entire configuration of your CORS middleware: allowed origins, allowed methods, allowed request headers, etc.
  • Correctness & safety: you cannot (modulo bugs) end up with a dysfunctional or insecure middleware: jub0bs/cors indeed rejects any configuration that it deems invalid or insecure, not just at middleware initialisation, but also at middleware reconfiguration. Your middleware will either receive a clean update or none at all.
  • Performance: at all times, a middleware’s configuration lives in memory, in data structures optimised for processing CORS requests; middleware reconfiguration still is relatively expensive, especially in heap allocations, but under the reasonable assumption that reconfiguration remains less frequent (at least by an order of magnitude) than middleware invocation, you can expect good overall performance.

Critical vulnerability in jub0bs/cors <= 0.1.2

Finally, allow me a short but important digression about security. One of the data structures that jub0bs/cors relies on is a specialised radix tree. As I was implementing middleware reconfigurability, I came to the distressing realisation that, because of a bug in my radix-tree implementation (no doubt introduced as a result of poor variable naming on my part), jub0bs/cors contained a critical vulnerability: in v0.1.2 and prior versions, some CORS middleware allow a range of untrusted Web origins. For example, specifying origin patterns https://foo.com and https://bar.com (in that order) would yield a middleware that would incorrectly allow untrusted origin https://barfoo.com. v0.8.0 of jub0bs/fcors is similarly vulnerable. This critical vulnerability is yet another cautionary tale that a code coverage of 100% doesn’t guarantee the absence of bugs.

Finding a critical vulnerability in one’s code has to be an open-source developer’s worst nightmare, but this episode was particularly embarrassing to me: not only do I describe myself as a security researcher but, only a few days earlier, I was bemoaning the leisurely pace at which the maintainer of rs/cors patched an altogether minor vulnerability and I was touting jub0bs/cors as perhaps the best CORS middleware library for Go yet… 😬

The study of Web security is a neverending lesson in humility. Despite one’s best intentions, every piece of software one writes is bound, at one stage or another, to contain vulnerabilities; what matters is how one responds to their discovery. In this case, I identified and fixed the bug on April 30th; for better timing, though, I held off publishing a security advisory, notifying the Go Vulnerability Database, and releasing a patched version (v0.1.3) until May 2nd (CEST).

I apologise to anyone impacted and, if you haven’t given jub0bs/cors a try yet, I hope you won’t be deterred.

Acknowledgements

Thanks to Laurent Demailly, Scott Plunkett, and Mike Stephen for the constructive feedback about my prospective API changes that they respectively shared with me on Gophers Slack, on Reddit, and through private communication.