43 minutes
Fearless CORS: a design philosophy for CORS middleware libraries (and a Go implementation)
TL;DR ¶
In this post, I investigate why developers struggle with CORS and I derive Fearless CORS, a design philosophy for better CORS middleware libraries, which comprises the following twelve principles:
- Optimise for readability
- Strive for a simple and cohesive API
- Provide support for Private Network Access
- Categorise requests correctly
- Validate configuration and fail fast
- Treat CORS as a compilation target
- Provide no default configuration
- Do not preclude legitimate configurations
- Ease troubleshooting by eschewing shortcuts during preflight
- Render insecure configurations impossible
- Guarantee configuration immutability
- Focus performance optimisation on middleware execution
This post also introduces jub0bs/fcors, an open-source, production-ready CORS middleware library for Go that adheres to Fearless CORS.
Familiarity with the CORS protocol is a prerequisite for reading this post; if you need to brush up on CORS, MDN Web Docs is a good resource. Some familiarity with Go, Java, and JavaScript is beneficial but not required. This post is quite long, but its structure should allow you to dip in and out of it as you please.
Introduction ¶
CORS 101 ¶
Cross-Origin Resource Sharing (CORS) is a mechanism
that lets servers instruct browsers to relax, for select clients,
some restrictions (in terms of both sending and reading)
enforced by the Same-Origin Policy (SOP)
on cross-origin network access.
The protocol relies on special HTTP headers such as
Origin
, Access-Control-Request-Method
, Access-Control-Allow-Origin
, etc.
Because a picture is worth a thousand words,
let me show you a typical example of a successful CORS handshake:
- Bob visits Carol’s website (
https://carol.com
). - Carol’s client uses the Fetch API to issue a
GET
request to a resource on Sarah’s server (https://sarah.com
). Carol includes a header namedAuthorization
in her request. - The request is such that Bob’s browser first issues a
preflight request
(which uses the
OPTIONS
method) to the server. - The server sends a preflight response,
which instructs Bob’s browser that Carol’s client is allowed
to send her actual (
GET
) request. - The browser sends Carol’s actual request to the server.
- The server sends back a response, which also instructs Bob’s browser that Carol’s client is allowed to read the response in question.
- The browser complies and gives Carol’s client access to Sarah’s response.
This is CORS in a nutshell. In practice, some daring server developers choose to implement CORS by “manually” setting the relevant HTTP response headers, either at the application level or at the reverse-proxy level; but doing so is error-prone. As expressed by Jake Archibald’s pithy statement,
CORS (Cross-Origin Resource Sharing) is hard.
CORS is, at the very least, more intricate than meets the eye. In practice, developers tend to rely instead on some middleware library, which can leverage the full power of their programming language to abstract some of CORS’s complexity.
CORS woes ¶
Although CORS has been, since its inception in the late 2000s, an instrumental mechanism for the development of the modern Web, it remains a frequent source of confusion and exasperation for developers. At the time of writing this post, the number of Stack Overflow questions tagged with “cors” hovers around 12,500, and a disconcertingly large fraction of those questions comes from a place of anguish. Some view fixing CORS issues as a rite of passage into Web-development mastery:
People wonder what it takes to be a senior developer.
— Ben (@digitaltrouble) April 9, 2021
One CORS issue. By the time you've fixed it, you will be a senior in any way possible... 😑
If you have never faced a CORS issue, are you even a web developer?
— Void⚡ (@codewithvoid) October 16, 2022
Others take to social media to give way to gloom or vent their frustration:
Fuck cors
— Developers Swearing (@gitlost) July 14, 2017
Of cors this would happen pic.twitter.com/tPjObgIBlC
— Cassidy (@cassidoo) August 6, 2019
I don't think I mentioned it today...
— Layla (@LaylaCodesIt) November 4, 2021
But in case you didn't know...
I HATE CORS!!
It's always CORS. The bane of my webdev existence. pic.twitter.com/Xv0Tm54O4a
CORS has to be the single must frustrating part of any development I do. I've solved it so many times and yet every time I build an API I still end up down a Google rabbit hole 🤷♂️
— James Eastham (@plantpowerjames) May 22, 2022
CORS issues keep me up at night.
— Andrew Brown (@andrewbrown) January 13, 2023
No matter how many times I build apps, I'm debugging CORS for hours upon hours... pic.twitter.com/DFAvTIzXQC
Productivity in software development is notoriously hard to measure, but obstacles to productivity are hard to ignore; and, if such public outcry as sampled above is anything to go by, one can only shudder at the thought of the cumulative time and money that has been wasted on troubleshooting CORS issues over the years.
There is a flip side to this: because CORS deactivates some browser defences, misconfiguring it can compromise, not just functionality, but also security. Insecure CORS configurations are perhaps less decried than dysfunctional ones but, when they do occur, they have the potential to expose stakeholders to devastating cross-origin attacks. In fact, server developers fighting a losing battle against a dysfunctional CORS configuration may be driven, as a last-ditch attempt to just “make things work”, to adopt an overly permissive CORS policy, such as one that throws most of the SOP out the window!
Why this continual weeping and gnashing of teeth? Is the protocol itself to blame for so much misery? No; I argue that CORS’s reputation as a productivity killer and security footgun is mostly undeserved. Are developers simply too dumb to bend CORS to their will, then? Developers in general would certainly benefit from familiarising themselves better with the protocol before using it or opting for farfetched and dangerous alternatives, but assigning the blame entirely to them would be unfair. What about the tools? Contrary to popular opinion, I think browsers do a rather fine job in helping developers troubleshoot their CORS issues. This only leaves CORS libraries out of account…
Out of the CORS tar pit ¶
Over the past few months, I’ve been driven to study the CORS protocol with more attention than I had formerly given to it. I’ve spent hours perusing current and old specifications, code-spelunking in both prominent and obscure CORS libraries, rummaging through pull requests and GitHub issues, reading reports of insecure CORS configurations, sifting through countless Stack-Overflow questions about CORS and answering them when I could… At last, I came to the conclusion that developers’ difficulties with CORS largely result from certain infelicities in CORS middleware libraries.
But identifying culprits isn’t enough: how shall we escape this predicament? In reaction to the shortcomings I perceive in existing CORS middleware libraries, I started gathering my thoughts about how I would design such a library from scratch, with the benefit of hindsight and unencumbered by unfortunate past design decisions or promises of backwards compatibility.
This post enunciate my vision for a better CORS middleware library, one that designs CORS misconfigurations—both dysfunctional and insecure configurations—out of existence. Nicknamed Fearless CORS, my design philosophy ramifies into twelve language-agnostic principles, in a manner reminiscent of REST’s six constraints and Heroku’s Twelve-Factor-App methodology. This post also presents jub0bs/fcors, a production-ready CORS middleware library for Go that adheres to Fearless CORS.
What follows is one third murder mystery, one third design manifesto, and one third billboard for jub0bs/fcors. I hope you enjoy it! My views may strike you as stubborn and contentious, and my tone as acerbic and pompous, but I’ve endeavoured to remain lucid, if only occasionally harsh, in my criticism of existing CORS libraries. Of course, I don’t have a monopoly on truth; if you disagree with Fearless CORS, I’d like to hear from you; and if you find a bug in jub0bs/fcors or identify a missing feature, please do file an issue on GitHub.
The twelve principles of Fearless CORS ¶
1. Optimise for readability ¶
Most CORS middleware libraries—to the notable exception of Spring’s and .NET Core’s—adopt an imperative style. Here is an example featuring rs/cors:
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
},
AllowedHeaders: []string{"Authorization"},
})
However, a declarative style tends to outmatch an imperative one in terms of readability of the resulting configuration code. Here is how you would express an equivalent CORS policy with jub0bs/fcors:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("https://example.com"),
fcors.WithMethods(
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
),
fcors.WithRequestHeaders("Authorization"),
)
The differences are more than cosmetic.
By contrast with rs/cors, jub0bs/fcors
requires less ceremony.
In particular, through its use of variadic function parameters,
jub0bs/fcors does away with those distracting
slice literals ([]string{}
).
Moreover, through its use of a pattern known in the Go community
as functional options,
jub0bs/fcors obviates the need for
exposing an intermediate configuration struct
and lets you express your desired CORS policy in a more declarative style
via a small embedded domain-specific language.
If you ignore the repeated occurrences of the package name (“fcors”) in
qualified identifiers,
you’ll likely find the configuration code largely free of visual noise.
By saving developers a few keystrokes in this manner, jub0bs/fcors can afford to err on the side of verbosity and self-documentation for the names of its options, e.g.
WithRequestHeaders
rather thanWithHeaders
,ExposeResponseHeaders
rather thanExposeHeaders
, andMaxAgeInSeconds
rather thanMaxAge
.
Not exposing any configuration struct presents another advantage:
the impossibility to build upon an existing CORS policy.
Although option values can be shared across calls to
jub0bs/fcors’s middleware factory functions (named
AllowAccess
and AllowAccessWithCredentials
),
this constraint tends to promote co-location of CORS-configuration code.
It also discourages the use of many different CORS policies,
which OWASP frowns upon anyway:
Implement access-control mechanisms once and re-use them throughout the application, including minimizing Cross-Origin Resource Sharing (CORS) usage.
As a result of these design decisions, configuration code written with jub0bs/fcors is comparatively easier to read and, hence, to review.
2. Strive for a simple and cohesive API ¶
In The Computer Scientist as Toolsmith II, the late Fred Brooks articulated an idea (slightly paraphrased here) that still resonates with me:
Two criteria for success in a tool are:
- It must be so easy to use that a seasoned developer can use it, and
- It must be so productive that seasoned developers will use it.
However, the size of a library’s API can hurt both ease of use and productivity. Joshua Bloch, author of Effective Java and of Java’s Collections Framework, insists that the best libraries distinguish themselves as having a small conceptual surface area, and enjoins library authors to maximise power-to-weight ratio. Likewise, John Ousterhout, author of A Philosophy of Software Design, argues that software modules—libraries, in this case—should be deep rather than shallow; in particular, between two libraries that have feature parity, the one whose interface is smaller is preferable:
Many CORS middleware libraries, no doubt due to their long development history,
are not as deep as they ideally could be.
For example, rs/cors’s Options
struct type
provides no fewer than three different ways
of expressing a CORS policy’s allowed origins:
AllowedOrigins
, of type[]string
;AllowedOriginFunc
, of typefunc (string) bool
; andAllowedOriginRequestFunc
, of typefunc (*http.Request, string) bool
.
This corner of rs/cors’s API strikes me as shallow.
In particular, AllowedOriginRequestFunc
subsumes—is strictly
more powerful than—AllowOriginFunc
.
This overlap in functionality between the two functions is unfortunate,
as it unnecessarily bloats the conceptual surface area of the library’s API.
I shall revisit those two function fields
further down in this post,
as I believe custom callbacks are misfeatures in their own right.
Besides, all three options lack self-documentation.
What happens if, say, AllowedOrigins
is used in conjunction with AllowOriginFunc
?
Are those options somehow additive? If not, which one has precedence over the other?
Definite answers to these questions are not immediately apparent;
you will only find them in the library’s documentation.
With jub0bs/fcors, developers can specify their allowed origins
only via two orthogonal and (mutually incompatible) options
named FromOrigins
and FromAnyOrigin
.
My library’s comparatively smaller API
also lends itself better to auto-completion by IDEs.
Moreover, in a bid to minimise clutter and dispel any ambiguity,
jub0bs/fcors opts for non-additive options and
raises a run-time error in the face of repeated and/or conflicting option calls:
cors, err := fcors.AllowAccess(
fcors.FromAnyOrigin(),
fcors.WithMethods("GET", "POST", "PUT"),
fcors.WithMethods("DELETE"), // ❌ (conflicts with earlier call)
)
3. Provide support for Private Network Access ¶
Private Network Access (PNA)
is a W3C initiative that strengthens the Same-Origin Policy
by denying clients in more public networks (e.g. the public Internet) access
to less public networks (e.g. localhost
);
the goal is to mitigate a class of cross-site-request-forgery attacks.
PNA also extends CORS by providing a server-side opt-in mechanism for such access.
At this stage, Private Network Access remains a moving target: the specification retains draft status and was recently renamed twice in the span of a few months. Nevertheless, PNA is gradually getting support in Chromium. Unfortunately, many CORS middleware libraries—Gin’s is but one example—still lack support for PNA, which forces their users to look elsewhere for a solution.
Besides, PNA happens to be a source of new difficulties for library authors. One assumption that used to be true is that browsers would issue a preflight request only in reaction to a client’s attempt to send a cross-origin request. Some CORS libraries, such as Gin’s, still actively rely on this assumption. In this regard, PNA throws a spanner in the works: as a mitigation against DNS rebinding, PNA-compliant browsers may now issue a preflight request even in reaction to a client’s attempt to send a same-origin request! Accordingly, the implementation of Gin’s CORS middleware library will require amendments, which may break clients who rely on the library’s current behaviour.
Because PNA is a natural extension of the CORS protocol,
jub0bs/fcors fully supports it
through advanced options hidden away in its risky
companion package.
Moreover, among
other similar constraints,
jub0bs/fcors prohibits developers
from enabling private network access from all origins,
a practice that the team leading the PNA specification effort
actively discourages:
The server can set
Access-Control-Allow-Origin: *
, though this is dangerous and discouraged. Private network resources should rarely be accessible to all origins, so think carefully about the risks involved in setting such a header.
4. Categorise requests correctly ¶
The Fetch standard tells us that a preflight request is one that, at the very least,
- uses the
OPTIONS
method, - includes an
Origin
header, - includes an
Access-Control-Request-Method
header.
In one of his posts,
Jake Archibald laments that some servers incorrectly
take the presence of an Origin
header as an unmistakable cue
that a request is cross-origin.
I am similarly saddened by CORS middleware
(like Express.js’s) that fail to recognise
that preflight requests form a strict subset of all OPTIONS
requests:
This blunder unfortunately traps non-preflight OPTIONS
requests,
which never make it past the CORS middleware
to the next HTTP handler in the chain.
Its deleterious effects also tend to reverberate throughout the CORS library.
Rather than identifying the problem and tackling it at the root,
the maintainers of Express.js’s CORS middleware
unfortunately opted instead to
retrofit the library to provide their users
a kludgey way of letting non-preflight OPTIONS
requests through, in the form of
a new configuration option named preflightContinue
.
The addition of an OptionsPassthrough
option
to rs/cors
and the addition of an IgnoreOptions
option
to gorilla/handlers were similarly motivated.
Such options are pure manifestations of accidental complexity
and make their library’s API harder to apprehend.
By contrast, because jub0bs/fcors
correctly categorises OPTIONS
requests,
it requires no such cruft.
5. Validate configuration and fail fast ¶
The sheer number of CORS middleware libraries that perform little to no configuration validation and unconditionally produce a middleware, even a dysfunctional one, baffles me. In this regard, those libraries truly do their users a disservice.
For example, developers not very familiar with the concept of Web origin may attempt to list, among the origins allowed by their CORS policy, a value that is actually not a valid origin, such as a URI that lacks a scheme or one that contains a path. The following code snippet draws inspiration from a real-life example involving Fiber, a Web framework for Go:
app.Use(cors.New(cors.Config{
AllowOrigins: "https://example.com/", // incorrect
})
For context, here is the signature of Fiber’s
factory function cors.New
:
func New(...cors.Config) fiber.Handler
The use of a variadic parameter is itself questionable,
but what I want to point out is that this function never “fails”:
it does not return an error, nor does it panic.
Instead, in my example, it completely overlooks the configuration issue
caused by the invalid origin value
and returns a middleware that happens to be dysfunctional
because that middleware rejects all CORS requests,
even those whose origin is https://example.com
.
Incidentally, I find this practice
of ignoring failure cases—or sweeping them under the rug—especially
puzzling when followed by Go libraries,
because diligent error reporting is so central to Go’s culture.
Users of CORS libraries that lack configuration validation may find out only much later that nothing works as expected, e.g. after having deployed their server and testing their client against it; and, even then, those developers may be at a loss to pinpoint the root cause of the dysfunction they observe. True, browsers do, in such cases, provide an illuminating error message; here is Chromium’s:
Access to fetch at [REDACTED] from origin ‘https://example.com’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The ‘Access-Control-Allow-Origin’ header has a value ‘https://example.com/' that is not equal to the supplied origin. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request’s mode to ’no-cors’ to fetch the resource with CORS disabled.
But this rather loose feedback loop is detrimental to developer experience. Immediately presenting developers with the brutal truth, in the form of a helpful server-side error message, would be preferable to delaying their disappointment. Accordingly, CORS middleware libraries should categorically refuse to produce a middleware bound to be dysfunctional and should error out as early as middleware instantiation.
Here is another example of a a general lack of configuration validation. Some CORS libraries, like rs/cors, allow their users to configure the status code that preflight responses should use, but few of them check that the status code in question is an ok status (i.e. an integer in the 200-299 range), which is a necessary condition for preflight to succeed:
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
OptionsSuccessStatus: 300, // incorrect
})
Still unconvinced that lack of configuration validation is problematic? Here is another example featuring the Spring framework:
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("Content-Type,Authorization"); // incorrect
}
This Java code snippet raises no exception, but the resulting CORS middleware
does not allow request headers Content-Type
and Authorization
. Why not?
Because Spring’s allowedHeaders
method is variadic
and expects its users to specify their allowed request-header names,
not as a single string value, but one by one as separate arguments:
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("Content-Type", "Authorization"); // correct
}
If Spring instead failed fast in the event of an invalid user-specified
request-header name—and Content-Type,Authorization
most definitely is not a valid request-header name—the
framework would spare its users much frustration.
In stark contrast with those libraries, jub0bs/fcors adopts a defensive approach: it steadfastly validates CORS configuration (origins, headers, methods, etc.) and errors out at middleware instantiation rather than produce a dysfunctional middleware.
Moreover, jub0bs/fcors guides developers unfamiliar with CORS towards a cruft-free configuration, by aggressively rejecting request headers, response headers, and method names that the Fetch standard forbids:
cors, err := fcors.AllowAccess(
fcors.FromAnyOrigin(),
fcors.WithMethods("CONNECT"), // ❌
fcors.WithRequestHeaders("Cookie"), // ❌
fcors.ExposeResponseHeaders("Set-Cookie"), // ❌
)
As a result of its strict stance on configuration validation, jub0bs/fcors enables a tighter development feedback loop than its competition does. The next principle expands on this idea, but in the specific context of the CORS protocol’s so-called wildcard exception.
6. Treat CORS as a compilation target ¶
One peculiarity of the CORS protocol that trips many Web developers up is
a deliberate constraint colloquially known as the wildcard exception.
The Fetch standard remains the authoritative (if somewhat dry) source of truth
about CORS, but MDN Web Docs does a good job at demystifying
the wildcard exception.
In a nutshell, credentialed requests (e.g. requests that include cookies)
are incompatible with uses of the wildcard (*
)
in any of the following CORS response headers:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Expose-Headers
Browsers simply deny clients access to such responses and raise an error, either during preflight (when applicable) or after receiving the response to the actual request from the server.
The wildcard exception indisputably is a beneficial constraint: it plays an important role in preventing incautious server developers from allowing credentialed access from more Web origins than they ought to, and, therefore, in protecting stakeholders from cross-origin attacks meant to exfiltrate their sensitive data.
You would expect a well-designed CORS middleware library to prohibit dysfunctional CORS configurations that disregard the wildcard exception. Unfortunately, many libraries don’t go through that trouble. Here is an example featuring the CORS middleware library of Express.js:
const express = require('express')
const cors = require('cors')
const app = express()
const port = 8081
const corsOptions = {
origin: '*',
credentials: true,
}
app.get('/hello', cors(corsOptions), (req, res) => {
res.send('Hello, World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Sending a GET
CORS request to /hello
yields
a response that contains the following headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
As a result, browsers will allow anonymous access from all origins,
but won’t allow credentialed access
precisely because of the wildcard exception.
Clients who send credentialed GET
requests to /hello
will indeed
be confronted with a CORS error of this kind:
Access to fetch at [REDACTED] from origin [REDACTED] has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.
You can imagine how this error message can elicit puzzlement and frustration among developers unfamiliar with the wildcard exception. In fact, seldom a day goes by without some distraught developers asking for help about the wildcard exception on Stack Overflow. Not a great developer experience…
A well-designed CORS middleware library would fail fast and would not allow server developers to proceed with such a dysfunctional CORS configuration as one that ignores the wildcard exception.
But developers’ misfortunes do not end there, because some CORS middleware libraries make even more regrettable design decisions. Well aware of the wildcard exception, those libraries bend over backwards to spare their users a dysfunctional CORS middleware and give them an insecure one instead! Here is an example featuring the Echo framework’s CORS middleware:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowCredentials: true,
}))
e.GET("/hello", hello)
e.Logger.Fatal(e.Start(":8081"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
Sending a GET
CORS request to /hello
from Web origin https://attacker.com
yields a response that contains the following headers:
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
😱 Yikes!
Echo’s CORS middleware actively bypasses the wildcard exception
by unconditionally reflecting the request’s origin
in the Access-Control-Allow-Origin
header,
thereby leaving the door wide open to cross-origin attacks!
This regrettable design decision has plagued and still plagues
many CORS middleware libraries,
to the point that such conscientious security researchers as
Evan Johnson and Jianjun Chen
felt compelled to publicly call some of them out, with varying degrees of success.
Edit (2023-02-28): I raised the issue on the Echo repository and the maintainers have since improved the situation a bit by removing this dangerous behaviour and adding an escape hatch to reinstate it if needed. Fiber, which was similarly afflicted, has also been fixed since the publication of this post.
By contrast with the great majority of CORS middleware libraries, jub0bs/fcors sidesteps the wildcard-exception difficulty altogether. In the spirit of John Ousterhout’s philosophy of software design, jub0bs/fcors pulls complexity downwards: it abstracts the complexity associated with the wildcard exception by treating CORS as a lower-level compilation target—an idea reminiscent of one that Mike West once floated about Content Security Policy:
: … perhaps we can start looking at CSP as a (complicated, low-level) compilation target. @hillbrad @randomdross
— Mike West (@mikewest) October 5, 2016
jub0bs/fcors indeed prevents dysfunctional configurations
by prohibiting explicit use of the wildcard ("*"
)
and forcing users to describe their desired CORS policy
in a higher-level, more declarative fashion instead.
For instance, the following code snippet raises a run-time error:
fcors.AllowAccess(
fcors.FromOrigins("*"), // ❌
)
Developers who wish to allow anonymous access from any origin should instead write
fcors.AllowAccess(
fcors.FromAnyOrigin(), // ✅
)
The library does use the wildcard under the hood when possible, but affords developers blissful oblivion to this implementation detail.
Besides, jub0bs/fcors embraces Yaron Minsky’s principle of making illegal states unrepresentable: far from insecurely bypassing the wildcard exception, jub0bs/fcors leverages Go’s type system in order to prevent (at compile time!) server developers from allowing credentialed access from all origins.
7. Provide no default configuration ¶
Most CORS middleware libraries provide some default configuration. For instance, rs/cors exports the following function, which returns a “default” CORS middleware:
// Default creates a new Cors handler with default options.
func Default() *Cors
As a prospective user of this function, you may find its documentation cryptic and ask yourself the following questions:
- What exactly are those “default options”?
- What behaviour does the resulting CORS middleware exhibit?
- Is the resulting CORS middleware suited for my needs?
To find definite answers, you’d have no other choice but to dig into the library’s source code.
Lacking documentation is one thing, but I want to draw your attention to a deeper issue. On the one hand, and contrary to popular belief, activating CORS never strengthens security but always weakens security (to varying degrees).
That so many people routinely conflate the Same-Origin Policy and CORS annoys me to no end... No metaphor is perfect but, if the former is akin to a seatbelt, the latter is the button that unbuckles that seatbelt. #infosec
— @jub0bs@infosec.exchange (also jub0bs.bsky.social) (@jub0bs) July 5, 2022
Some widespread CORS configurations may strike you as innocuous.
For instance, you may perceive a blanket Access-Control-Allow-Origin: *
policy
as a harmless default, but even such an innocent-looking CORS policy
can prove insecure in some contexts.
On the other hand, configuration in general should be
secure by default.
From this apparent conflict, I see but one escape: the only sane default consists in not enabling CORS at all. Thefore, my stance is that a well-factored CORS middleware library should not provide any default configuration. Instead, it should, at the cost of some verbosity, force users to be deliberate about their CORS configuration.
Accordingly, jub0bs/fcors provides no default configuration. For instance, developers who wish to allow anonymous access from any origin must be deliberate and explicit about it in their CORS configuration:
cors, err := fcors.AllowAccess(
fcors.FromAnyOrigin(),
)
8. Do not preclude legitimate configurations ¶
Some CORS middleware libraries are rife with false affordances, i.e. things that seem feasible but are actually not. Below are two examples of false affordances that preclude legitimate CORS configurations; I’ve also included, as a digression, a third false affordance that does not stand in the way of legitimate CORS configurations but may nonetheless breed confusion.
Impossibility to turn preflight caching off ¶
To reduce traffic,
browsers feature an internal cache dedicated to preflight responses.
Developers can tune the expiration time of an individual preflight response
in the preflight cache
by including an Access-Control-Max-Age
header in that response
and specifying the desired max age (in seconds) as a nonnegative integer:
Access-Control-Max-Age: 30
Omitting the Access-Control-Max-Age
header altogether
causes browsers to cache the preflight response in question
for a default duration of five seconds,
whereas specifying a max-age value of 0
instruct browsers
not to cache the preflight response at all:
Access-Control-Max-Age: 0
Unfortunately, rs/cors, among other libraries, ignores this distinction and, therefore, altogether prevents its users from disabling preflight caching, should they wish to do so.
The "functional options" pattern is superior to a config struct in many ways, but one of its benefits is that it allows package authors to draw a distinction between zero value and default behaviour. #golang
— @jub0bs@infosec.exchange (also jub0bs.bsky.social) (@jub0bs) October 18, 2022
A telling example is CORS's Acess-Control-Max-Age response header:
— @jub0bs@infosec.exchange (also jub0bs.bsky.social) (@jub0bs) October 18, 2022
- Setting it with a value of 0 (int's zero value) leads to no caching of the preflight response.
- Omitting it altogether leads browsers to cache the preflight with a default value of 5 seconds.
Yet most #golang CORS middleware that use a config struct rather than functional options interpret a 0 max-age value as a cue to omit that header in the preflight response. 🤷
— @jub0bs@infosec.exchange (also jub0bs.bsky.social) (@jub0bs) October 18, 2022
jub0bs/fcors doesn’t suffer from this limitation:
cors, err := fcors.AllowAccess(
fcors.FromAnyOrigin(),
fcors.WithRequestHeaders("Authorization"),
fcors.WithMaxAgeInSeconds(0), // disables preflight caching
)
Violation of the case sensitivity of method names ¶
Several CORS middleware libraries normalise HTTP-method names to uppercase. Those libraries, either inadvertently or deliberately, neglect the fact that, according to the Fetch standard, method names are case-sensitive; this illustrative instance of Jake Archibald’s CORS playground should be enough to convince you.
Such unwarranted case normalisation causes problems for clients
that send requests
whose method is not uppercase—and not some case-insensitive match for one of
DELETE
, GET
, HEAD
, OPTIONS
, POST
, or PUT
, names for which
the Fetch standard carves out an exception.
Here is an example featuring rs/cors:
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"patch"},
})
Assume then that a client running in the context
of Web origin https://example.com
in a Fetch-compliant browser
sends a patch
request (note the lower case) to the server.
As rs/cors
internally normalises method names to uppercase,
the preflight response would contain
Access-Control-Allow-Methods: PATCH
as opposed to
Access-Control-Allow-Methods: patch
Because method names are case-sensitive, browsers rule this as a mismatch and fail CORS preflight with an error message of this kind:
Access to fetch at [REDACTED] from origin ‘https://example.com’ has been blocked by CORS policy: Method patch is not allowed by Access-Control-Allow-Methods in preflight response.
Very confusing! By contrast, jub0bs/fcors respects the case sensitivity of HTTP methods and therefore never gives rise to this kind of problem.
Reliance on an inadequate duration type to represent the max age ¶
Some CORS libraries let their users specify a max-age value via a duration type that supports sub-second precision; in particular,
- ASP.NET Core’s CORS middleware relies on the
TimeSpan
class, and - Gin’s CORS middleware relies on the
time.Duration
type.
Unfortunately, such libraries mislead their users into thinking that max-age values representing a fractional number of seconds (e.g. 3.5s) will be honoured; per the Fetch standard, max age must indeed be specified (if at all) as a whole number of seconds. In practice, those CORS libraries silently truncate user-specified max-age values to the nearest second, and such a silent truncation may surprise users not very familiar with the CORS protocol.
To avoid any such confusion, jub0bs/fcors represents max age,
not as a time.Duration
,
but as one of Go’s built-in unsigned integer types.
9. Ease troubleshooting by eschewing shortcuts during preflight ¶
CORS is configured on the server side, but applied by the browser on the client side. Because the CORS procotol involves two actors (three if you count the browser), figuring out how to fix a specific CORS issue remains challenging. Despite recent and commendable efforts by the Chromium team in that area (see Jecelyn Yeen’s DevTools State of the Union 2022), troubleshooting CORS issues can still slow down Web development to a snail’s pace.
In practice, a misconfigured CORS configuration manifests itself as an error in the browser’s Console tab, much to the chagrin of developers who often perceive those error messages as puzzling and unhelpful:
CORS is probably one of the most frustrating things I've worked with. It would be nice if it gave you an error message on the console.
— Matt Colyer (@mcolyer) July 22, 2012
In my opinion, however, this bad reputation is undeserved: most browsers, such as Firefox, indeed do boast a rich variety of informative CORS error messages. Unfortunately, most CORS middleware libraries are implemented in such a way that they give rise to only a small subset of those error messages in the browser, thereby masking the root cause of many CORS issues. For instance, a CORS error message that developers typically have to face is the following:
Access to fetch at [REDACTED] from origin [REDACTED] 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.
This error message is so common that the last sentence is contained verbatim in
roughly 20% of the questions tagged with “cors” on Stack Overflow.
In many cases, though, the issue stems, not from the origin of the request,
but from a request header that happens to be disallowed
by the server’s CORS policy.
The difficulty in troubleshooting this kind of issues is compounded by
client libraries like Axios which, in some cases,
silently include headers, such as Content-Type
, to requests.
To be absolutely fair, I must concede that the blame for this sad state of affairs likely rests, not with CORS middleware libraries themselves, but with the original CORS specification (since obsoleted by the Fetch standard), namely the W3C Cross-Origin Resource Sharing working draft. The version dated 17 March 2009 (☘️) of that working draft saw the addition of a section entitled Server Processing Model, which, among other things, prescribes how servers should handle preflight requests. Among those normative requirements, here is the passage (only slightly redacted) that is most relevant to the present discussion:
- If [the value of the
Access-Control-Request-Method
header] is not a case-sensitive match for any of [the allowed methods], do not set any additional headers and terminate this set of steps. […]- If any of the [header names listed in the
Access-Control-Request-Headers
header] is not a ASCII case-insensitive match for any of the [allowed headers], do not set any additional headers and terminate this set of steps. […]- If the URL supports credentials, add a single
Access-Control-Allow-Origin
header, with the value of theOrigin
header as value, and add a singleAccess-Control-Allow-Credentials
header with the case-sensitive string “true
” as value. Otherwise, add a singleAccess-Control-Allow-Origin
header, with either the value of theOrigin
header or the string “*
” as value.
In essence, the W3C working draft prescribes servers take shortcuts
during preflight: servers ought to omit all CORS headers
(Access-Control-Allow-Origin
too!) from the response if preflight fails.
This leaves very little contextual information to the browser, which can only
complain about the absence of the Access-Control-Allow-Origin
header—because
that header happens to be the first CORS header to get processed
during a CORS check.
Here is an example.
Assume that Sarah configures CORS on her server (https://sarah.com
)
to allow Carol’s client (https://carol.com
) with a request header named Foo
.
In her request to Sarah’s server, Carol happens to include a header named Bar
,
which Sarah’s CORS policy does not allow:
const headers = {
'Foo': 'whatever',
'Bar': 'yolo',
};
fetch('//sarah.com', {headers: headers})
.then(res => res.text())
.then(console.log);
Sarah’s CORS middleware then sees,
listed in the Access-Control-Request-Headers
header of the resulting preflight request,
a header name (bar
) that it doesn’t allow:
OPTIONS / HTTP/1.1
Host: sarah.com
Origin: https://carol.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: bar,foo
-snip-
And because Sarah’s CORS middleware follows the W3C’s normative requirements for servers, it replies with a 403 status and omits all CORS headers from the preflight response!
HTTP/1.1 403 Forbidden
-snip-
Even though Sarah does allow the Web origin of Carol’s client in her CORS policy, when preflight fails, the browser presents Carol with an infuriatingly confusing CORS error message:
Access to fetch at ‘https://sarah.com’ from origin ‘https://carol.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.
This misleading message hides the root cause of the problem:
Carol’s use of a request-header name (Bar
) that Sarah’s CORS policy happens
to disallow.
The W3C’s normative requirements for servers endured through subsequent versions of the working draft more or less unchanged; and, because initial development of many CORS middleware libraries was contemporaneous with the W3C working draft’s heyday, most of those libraries—to the notable exception of Express.js’s—diligently complied and never looked back.
In my view, those problematic normative requirements for servers are the main reason why so many server developers have been and still are routinely led astray by CORS error messages.
In this context, taking such shortcuts and failing fast is actually detrimental, because it often robs developers of a good explanation as to what caused their CORS issues.
In my earlier example, if Sarah’s middleware instead responded to Carol’s preflight request with
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://carol.com
Access-Control-Allow-Headers: foo
then the browser would still fail preflight but would present Carol with a much more actionable CORS error message:
Access to fetch at ‘https://sarah.com’ from origin ‘https://carol.com’ has been blocked by CORS policy: Request header field bar is not allowed by Access-Control-Allow-Headers in preflight response.
Carol would then immediately realise that she needs to
either drop the Bar
header from her request,
or kindly ask Sarah to also allow that request header in her CORS policy.
Rather than freeing themselves from the W3C’s injunctions and getting rid of shortcuts during preflight, some libraries, such as rs/cors, elected to add support for logging in a bid to ease troubleshooting. However, I find such a decision less defensible in light of the changes brought by the Fetch standard. By comparison with the W3C working draft, the Fetch standard is indeed far less prescriptive about how servers ought to handle preflight requests:
Ultimately server developers have a lot of freedom in how they handle HTTP responses and these tactics can differ between the response to the CORS-preflight request and the CORS request that follows it […]
Interestingy, getting rid of such shortcuts was once suggested for but never implemented in rs/cors. A missed opportunity, if you ask me.
Sometimes you feel like throwing your hands up in the air, but jub0bs/fcors has got the love you need to see you through! Because my library closely follows the CORS-preflight algorithm and eschews undue shortcuts, you should find CORS issues much easier to troubleshoot with it than with other libraries.
10. Render insecure configurations impossible ¶
Earlier in this post, I stressed that CORS libraries should not produce dysfunctional middleware in the event of a misconfiguration. There’s a flip side to this. One of Joshua Bloch’s design principles that has stayed with me ever since I encountered it is the following:
A good library is one that is not only easy to use but hard to misuse.
The second part of this maxim is especially relevant when the library in question is concerned with such security-critical mechanisms as CORS.
I hope you will forgive this somewhat infantilising metaphor: a seat belt should arguably be designed in such a way that no unattended toddler can unbuckle it; similarly, a good CORS library should prevent developers from producing dangerously permissive CORS middleware.
Unfortunately, too many CORS middleware libraries fail to effectively protect developers from themselves because they insufficiently rein in their power.
I’ve already touched upon this topic earlier in this post (see Provide support for Private Network Access and Treat CORS as a compilation target), but here are more ways in which CORS middleware libraries should prevent their users from shooting themselves in the foot.
Do not support the null
origin ¶
Security researcher James Kettle
warns us (and rightly so!)
that allowing the null
origin in one’s CORS configuration
is rarely (if ever) a good idea;
the only justifiable use case I can think of
is a deliberately vulnerable resource in the context of
a Web-security lab about CORS misconfiguration.
Yet CORS libraries, by and large,
are quite content to tolerate the null
origin.
By contrast, jub0bs/fcors completely forbids the null
origin;
attempts to allow it result in a run-time error:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("null"), // ❌
)
Disallow insecure origins by default ¶
Another, perhaps more subtle, CORS misconfiguration consists in allowing
insecure origins, e.g. origins whose scheme is http
as opposed to https
,
such as http://example.com
.
As demonstrated by James Kettle,
doing so enables an active network attacker to steal
potentially sensitive data from his/her victim,
even if the latter only ever willingly interacts
with the vulnerable resource over a secure connection (i.e. using HTTPS).
Unfortunately, none of the CORS libraries I’ve reviewed guard their users
against this pitfall.
Even if you’re aware of it, you are a mere one-character typo away
from misconfiguring your CORS middleware,
and no such tiny typo should be this consequential.
Beyond secure defaults, a piece of software should ideally be designed in such a way that an insecure configuration always require more keystrokes than a secure configuration. Looking at you, CORS middleware libraries... 😇 #infosec
— @jub0bs@infosec.exchange (also jub0bs.bsky.social) (@jub0bs) February 5, 2022
Again, jub0bs/fcors has your back
and disallows most http
origins by default;
it does carve out an exception for
origins whose host is localhost
or a loopback IP address, though,
because such origins are typically harmless
and so commonly used in pre-prod environments.
By default,
attempts to allow one or more insecure origins result in a run-time error:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("http://example.com"), // ❌
)
If you need to deliberately allow one or more insecure origins, jub0bs/fcors lets you do so under the condition that you also activate an advanced option provided by its companion package, aptly named “risky”:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("http://example.com"), // ✅
risky.TolerateInsecureOrigins(),
)
Disallow dangerous origin patterns ¶
It is common among CORS libraries, such as Express.js’s, to support regular expressions as a means to allow a set of origins, e.g. all subdomains of some base domain:
/^https:\/\/.*\.example\.com$/
This feature affords a great deal of flexibility… perhaps too much. Security misconfigurations due to flawed regexps are indeed so common that it puts the judiciousness of such a feature into question. Even seasoned regexp wranglers can sometimes be faulted, e.g. when they forget to escape periods standing for label separators or to anchor their regexp at both ends:
/^https:\/\/.*.example.com$/
/^https:\/\/.*\.example\.com/
Besides, some regular-expression engines—though thankfully not Go’s—are prone to catastrophic backtracking, which, if exploited by an attacker on a vulnerable server, can cause a denial of service. Therefore, I don’t think CORS libraries should support regexps. Rob Pike likely would agree:
Regular expressions are, in my experience, widely misunderstood and abused.
Some CORS middleware libraries, like rs/cors, deserve praise for
refusing to support regexps
and supporting only a limited variety of pattern types,
but most of them still stop short of guarding their users
against excessively permissive patterns.
For instance, rs/cors tolerates https://*
as an origin pattern,
which is insecure because it matches any https
origin,
including, say, https://attacker.com
!
jub0bs/fcors is adamant in restricting the types of origin patterns it supports. Some examples follow:
https://*.example.com
, in which the asterisk marks exactly one DNS label;https://**.example.com
, in which the sequence composed of two asterisks marks one or more DNS labels;https://example.com:*
, in which the asterisk marks an arbitrary (possibly implicit) port number.
jub0bs/fcors supports no other types of origin patterns. In particular,
all of the following patterns are illegal and cause middleware instantiation to fail:
https://*
, https://example.*
, https://*.example.com:*
.
jub0bs/fcors even goes the extra mile for your safety:
by default, it prevents developers from
inadvertently allowing arbitrary subdomains
of a public suffix (e.g. com
),
because such domains (e.g. random-attacker-666.com
)
are by definition registrable by anyone, including malicious actors.
By default, attempts to allow arbitrary subdomains of a public suffix
result in a run-time error:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("https://*.com"), // ❌
)
Again, if you need to deliberately allow arbitrary subdomains of one or more public suffixes, you’ll need to explicitly open another escape hatch provided by the risky package:
cors, err := fcors.AllowAccess(
fcors.FromOrigins("https://*.com"), // ✅
risky.SkipPublicSuffixCheck(),
)
Do not support custom callbacks ¶
I’ve kept the most dangerous misfeature of them all for last: custom callbacks. They come in numerous CORS libraries under different names and shapes:
- Express.js’s CORS configuration object accepts a callback (or even a boolean!)
for its
origin
property; - rs/cors’s configuration struct exports
a function field named
AllowOriginFunc
of signaturefunc(string) bool
; - Echo’s CORS middleware’s configuration struct exports a function field
also named
AllowOriginFunc
but of signaturefunc(string) (bool, error)
; - ASP.NET Core provides a
method named
SetIsOriginAllowed
that takes a parameter of typeFunc<string,bool>
; - the now unmaintained gorilla/handlers project similarly provides
an option named
AllowedOriginValidator
, etc.
By letting their users specify custom callbacks, those CORS middleware libraries give their users near total flexibility for specifying how CORS requests should be accepted or rejected, e.g. by querying some database for the list of allowed origins, as suggested by the documentation of Express.js’s CORS middleware:
var corsOptions = {
origin: function (origin, callback) {
// db.loadOrigins is an example call to load
// a list of origins from a backing database
db.loadOrigins(function (error, origins) {
callback(error, origins)
})
}
}
This “DIY” approach to CORS often leads to frightful results,
precisely because custom callbacks can so easily be abused by developers.
For example, I’ve seen many developers specify
a predicate that unconditionally returns true
while also allowing credentialed requests,
thereby unwittingly opening the door to authenticated cross-origin attacks
from any Web origin; here is an example using rs/cors:
c := cors.New(cors.Options{
AllowOriginFunc: func (origin string) bool { return true }, // 😱
AllowCredentials: true,
})
There’s an even more pernicious form of custom callbacks, present in both rs/cors and Chi (yet another router for Go), which grants the predicate access to the request object:
func(*http.Request, string) bool
Motivations for this feature include the desire to vary responses
on the basis of the presence and/or value of some request headers,
such as Cookie
, Authorization
, or some non-standard HTTP header.
Do you see anything amiss? The answer is quite subtle…
Perhaps the most glaring issue with that predicate’s signature is that,
since the request’s origin is accessible
via the *http.Request
parameter anyway,
the string parameter is redundant;
another manifestation of accidental complexity right there.
However, one much more severe issue with such a predicate is that
it’s almost guaranteed to lead to cache poisoning!
If you vary responses on the basis of the presence and/or value of some request header,
HTTP indeed mandates that you list the name of that header in your responses’
Vary
header;
doing this signals to caching intermediaries that the header in question
should be part of the cache key.
But because the predicate lacks
a http.ResponseWriter
parameter,
it gives you no way of populating the Vary
header as required!
You have to remember to manually do so outside of the predicate, somehow.
If you forget,
as I believe most developers do,
to adequately populate the Vary
header,
caching intermediaries are then liable to serve
inappropriate (and possibly malicious) cached responses to your clients.
The examples above may be sufficient to convince you that CORS middleware libraries are better off staying away from those custom-callback features. In my experience, most use cases for CORS don’t require this level of customisation anyway.
By contrast, jub0bs/fcors curtails developers’ power (for their own good)
and eschews all forms of custom callbacks.
Moreover,
as discussed earlier in this post,
its FromAnyOrigin
option is rendered incompatible
(at compile time!) with credentialed access.
11. Guarantee configuration immutability ¶
Edit (2024/05/15): I have since softened my stance a bit on this one.
Some CORS middleware libraries support dynamic updates to CORS configuration, thereby obviating the need for a server redeployment. Here is a proof of concept featuring Express.js:
const express = require('express')
const cors = require('cors')
const app = express()
const port = 3000
const corsOptions = {
origin: 'http://example.com',
}
app.use(cors(corsOptions));
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.post('/change-allowed-origin', function (req, res, next) {
corsOptions.origin = 'http://example.org';
res.send('Done!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Once that server has been started,
sending a single POST
request to /change-allowed-origin
will indeed cause an update of the server’s CORS configuration:
the allowed origin, which beforehand was https://example.com
will become https://example.org
.
$ curl -sD - -o /dev/null \
-H "Origin: https://example.org" \
localhost:3000
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://example.com
Vary: Origin
-snip-
$ curl -XPOST localhost:3000/change-allowed-origin
Done!
$ curl -sD - -o /dev/null \
-H "Origin: https://example.org" \
localhost:3000
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://example.org
Vary: Origin
-snip-
Whether this affordance is intentional or incidental (and due to a lack of defensive copying) is unclear to me. Though expedient and sometimes desirable, this affordance is arguably more harmful than good: it opens the door to race conditions if the server processes requests (and invokes middleware) in a concurrent fashion, as many servers do.
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. Custom callbacks and insufficient defensive copying stand in the way of this principle. Besides, 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 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.
12. Focus performance optimisation on middleware execution ¶
As discussed earlier in this post, jub0bs/fcors performs more validation at startup than most other CORS libraries do; moreover, it relies heavily on the functional-options pattern at startup, which is less performant than simply exposing a configuration struct type to developers. Therefore, building a CORS middleware with jub0bs/fcors is comparatively slower and more memory-hungry, though not prohibitively so (only by a factor of about 20), than building an equivalent middleware with rs/cors:
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
rs_cors______startup 2016933 590 ns/op 371 B/op 8 allocs/op
jub0bs_fcors_startup 85252 12130 ns/op 6664 B/op 76 allocs/op
In my opinion, that’s tolerable. For HTTP middleware, performance at initialisation indeed matters less than performance at execution: incurring a modest performance penalty when initialising a middleware is acceptable if invocations of that middleware require relatively few CPU cycles and cause inconsequential amounts of heap allocations.
jub0bs/fcors makes such a tradeoff: it frontloads as much of the necessary work as possible at middleware initialisation in order to spare the hot path, i.e. middleware execution. That initial investment, both in time and memory, is typically recouped after only a modest number of middleware invocations. And if your use case is such that server startup shall not suffer even a few microseconds’ delay, you can always riff on Mat Ryer’s tricks for lazy middleware initialisation.
You may be tempted to relegate execution time of a middleware to secondary importance, because network I/O is likely to be the dominating factor in performance. However, this is only true if middleware invocations remain inexpensive; and many CORS middleware libraries, in part because they give developers too much freedom, simply cannot provide such guarantee.
As often in software design, constraints liberate. By restricting the kind of origin patterns that developers can specify in their CORS configuration, jub0bs/fcors is free to rely on a custom tree-like data structure designed for fast origin lookup. And by denying developers a means to specify their CORS policy as a custom callback, jub0bs/fcors prevents them from inadvertently carrying out expensive computations (such as compiling a regexp) or I/O-intensive tasks (such as querying a database) during execution of their CORS middleware.
The consequence of those deliberate constraints is that invocations of a middleware built by jub0bs/fcors are fast and only incur few heap allocations.
Edit (2023/08/02): Below are some results—only slightly redacted—of microbenchmarks comparing two functionally equivalent CORS middleware, one built with rs/cors and the other with jub0bs/fcors:
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
without_CORS 205514234 6 ns/op 0 B/op 0 allocs/op
# rs/cors
sgl__vs_actual 3194002 375 ns/op 48 B/op 3 allocs/op
mult_vs_actual 3152779 382 ns/op 48 B/op 3 allocs/op
any__vs_actual 3499288 342 ns/op 48 B/op 3 allocs/op
sgl__vs_preflight 1223632 983 ns/op 160 B/op 6 allocs/op
mult_vs_preflight 1214544 986 ns/op 160 B/op 6 allocs/op
any__vs_preflight 1264927 951 ns/op 160 B/op 6 allocs/op
any__vs_preflight_hdr 907146 1302 ns/op 208 B/op 10 allocs/op
# jub0bs/fcors
sgl__vs_actual 21439983 53 ns/op 0 B/op 0 allocs/op
mult_vs_actual 6499868 184 ns/op 0 B/op 0 allocs/op
any__vs_actual 1453963 53 ns/op 0 B/op 0 allocs/op
sgl__vs_preflight 7923229 152 ns/op 0 B/op 0 allocs/op
mult_vs_preflight 4998284 222 ns/op 0 B/op 0 allocs/op
any__vs_preflight 8044881 149 ns/op 0 B/op 0 allocs/op
any__vs_preflight_hdr 7042791 171 ns/op 0 B/op 0 allocs/op
As you can see, jub0bs/fcors is nimble and purrs quietly.
Conclusion ¶
Thank you for your attention and your patience through this long and winding post! At this stage, I hope that at least some of my arguments for Fearless CORS have swayed you.
If you’re a Gopher in need of a dependable CORS middleware library, you are welcome to take jub0bs/fcors for a spin. I’ll wait (an indefinite amount of time) for feedback before releasing version 1.0.0 but, as far as I’m concerned, the library is feature-complete and production-ready. Please let me know on Bluesky or Mastodon if it makes your life easier!
Although I’ve endeavoured to avoid past design mistakes of others, I’ve likely made brand new ones myself. If you disagree with Fearless CORS, or perceive shortcomings (bugs, missing features, misfeatures, etc.) in jub0bs/fcors, please open an issue on GitHub
Finally, if your language of choice isn’t Go and lacks a good CORS library, feel free to draw inspiration from Fearless CORS to implement a better CORS library for that language; and do let me know how it goes!
Acknowledgements ¶
I’m indebted to both Michael Smith (a.k.a. sideshowbarker on Stack Overflow and elsewhere) and Mat Ryer for generously taking the time to read an early draft of this particularly lengthy post and giving me valuable tips on how to improve it.
I’m also grateful to Anne Van Kesteren and Jake Archibald for clarifying aspects of the CORS protocol that initially tripped me up, and to Titouan Rigoudy for elucidating some of the finer points of Private Network Access.
Finally, although I’ve never interacted with them directly, Joshua Bloch and John Ousterhout, through their books and talks, had a profound impact on Fearless CORS.
API designCORSGoconfigurationcross-origin resource sharingfunctional optionslibrarymiddlewareoriginperformancesecurity
9067 Words
2023-02-08 13:00 +0000