cors

package module
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2025 License: MIT Imports: 12 Imported by: 5

README

jub0bs/cors

tag Go Version Go Reference license build codecov goreport

A principled CORS middleware library for Go, designed to be both easier to use and harder to misuse than existing alternatives.

About CORS

The Same-Origin Policy (SOP) is a security mechanism that Web browsers implement to protect their users. In particular, the SOP places some restrictions on cross-origin network access, in terms of both sending and reading.

Cross-Origin Resource Sharing (CORS) is a protocol that lets servers instruct browsers to relax those restrictions for select clients.

Features

This library allows you to configure and build net/http middleware that implement CORS. It distinguishes itself from other CORS middleware libraries by providing the following features:

Despite all of this library's goodness, you may still have valid reasons for favoring libraries like the more popular rs/cors. Here is as exhaustive a list as I could come up with:

Installation

go get github.com/jub0bs/cors

This library requires Go 1.23 or above.

Example

The following program demonstrates how to create a CORS middleware that

  • allows anonymous access from Web origin https://example.com,
  • with requests whose method is either GET or POST (or HEAD), and
  • (optionally) with request header Authorization,

and how to apply the middleware in question to all the resources accessible under the /api/ path:

package main

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

  "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // note: not configured for CORS

  // create CORS middleware
  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) // turn debug mode on (optional)

  api := http.NewServeMux()
  api.HandleFunc("GET  /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api))) // note: method-less pattern here

  if err := http.ListenAndServe(":8080", mux); err != http.ErrServerClosed {
    log.Fatal(err)
  }
}

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

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

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

Try it out yourself by saving this program to a file named server.go. You may need to adjust the port number if port 8080 happens to be unavailable on your machine. Then build and run your server:

go build server.go
./server

If no error occurred, the server is now running on localhost:8080 and the various resources accessible under the /api/ path are now configured for CORS as desired.

If you need to handle CORS-configuration errors programmatically, see package cfgerrors.

A note about testing

Be aware that, for performance reasons, CORS middleware produced by this library closely adheres to guarantees (provided by the Fetch standard) about the format of some CORS headers. In particular, if you wish to write tests that exercise CORS middleware via CORS-preflight requests that include an Access-Control-Request-Headers header, keep in mind that you should specify the comma-separated elements in that header value

  • in lower case,
  • in lexicographical order,
  • without repetitions.

Otherwise, the CORS middleware will cause preflight to fail.

Documentation

The documentation is available on pkg.go.dev.

Moreover, guidance on how to use this library with popular third-party routers can be found in jub0bs/cors-examples.

Code coverage

coverage

Benchmarks

Some benchmarks pitting this library against rs/cors are available in jub0bs/cors-benchmarks.

License

All source code is covered by the MIT License.

Additional resources

Documentation

Overview

Package cors provides net/http middleware for Cross-Origin Resource Sharing (CORS).

This package performs extensive configuration validation in order to prevent you from inadvertently creating dysfunctional or insecure CORS middleware.

Even so, care is required for CORS middleware to work as intended. Be particularly wary of negative interference from other software components that play a role in processing requests and composing their responses, including intermediaries (proxies and gateways), routers, other middleware in the chain, and the ultimate handler. Follow the rules listed below:

  • Because CORS-preflight requests use OPTIONS as their method, you SHOULD NOT prevent OPTIONS requests from reaching your CORS middleware. Otherwise, preflight requests will not get properly handled and browser-based clients will likely experience CORS-related errors. The testable examples associated with the *Middleware.Wrap method provide more guidance about avoiding such pitfalls when you rely on Go 1.22's enhanced routing features.
  • Because CORS-preflight requests are not authenticated, authentication SHOULD NOT take place "ahead of" a CORS middleware (e.g. in a reverse proxy or in some middleware further up the chain). However, a CORS middleware MAY wrap an authentication middleware.
  • Intermediaries SHOULD NOT alter or augment the CORS request headers that are set by browsers. Regarding the value of list-based field Access-Control-Request-Headers specifically, intermediaries MAY add some optional whitespace around the value's elements or add (inadvertently, perhaps) some empty elements to that value, but they SHOULD do so within reason; moreover, intermediaries MAY split the value of that field across multiple field lines of that name, but they SHOULD NOT add too many empty field lines of that name. For performance (and at the cost of some interoperability), this library's middleware are indeed stricter in their handling of this specific list-based field than required by RFC 9110.
  • Intermediaries SHOULD NOT alter or augment the CORS response headers that are set by this library's middleware.
  • Intermediaries MAY alter the value of the Vary header that is set by this library's middleware, but they MUST preserve all of its elements.
  • Multiple CORS middleware MUST NOT be stacked.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Origins         []string
	Credentialed    bool
	Methods         []string
	RequestHeaders  []string
	MaxAgeInSeconds int
	ResponseHeaders []string
	ExtraConfig
	// contains filtered or unexported fields
}

A Config configures a Middleware. The mechanics of and interplay between this type's various fields are explained below. Attempts to use settings described as "prohibited" result in a failure to build the desired middleware.

Origins

Origins configures a CORS middleware to allow access from any of the Web origins encompassed by the specified origin patterns:

Origins: []string{
  "https://example.com",
  "https://*.example.com",
},

Security considerations: Bear in mind that, by allowing Web origins in your server's CORS configuration, you engage in a trust relationship with those origins. Malicious actors, by exploiting some Web vulnerabilities (including cross-site scripting and subdomain takeover) on those origins, may be able to gain a foothold on those origins and mount cross-origin attacks against your users from there. Therefore, you should (in general) exercise caution when deciding which origins to allow. In particular, if you enable credentialed access and/or Private-Network Access, you should only allow Web origins you absolutely trust.

Omitting to specify at least one origin pattern is prohibited; so is specifying one or more invalid or prohibited origin pattern(s).

All valid schemes (no longer than 64 bytes) other than file are permitted, with one caveat about schemes other than https explained further down:

http://example.com    // permitted
https://example.com   // permitted
connector://localhost // permitted
file:///somepath      // prohibited

Origins must be specified in ASCII serialized form; Unicode is prohibited:

https://example.com            // permitted
https://www.xn--xample-9ua.com // permitted (Punycode)
https://www.résumé.com         // prohibited (Unicode)

Because the null origin is fundamentally unsafe, it is prohibited.

Hosts that are IPv4 addresses must be specified in dotted-quad notation:

http://255.0.0.0  // permitted
http://0xFF000000 // prohibited

Hosts that are IPv6 addresses must be specified in their compressed form:

http://[::1]:9090                                     // permitted
http://[0:0:0:0:0:0:0:0001]:9090                      // prohibited
http://[0000:0000:0000:0000:0000:0000:0000:0001]:9090 // prohibited

Valid port values range from 1 to 65,535 (inclusive):

https://example.com       // permitted (no port)
https://example.com:1     // permitted
https://example.com:65535 // permitted
https://example.com:0     // prohibited
https://example.com:65536 // prohibited

Default ports (80 for http, 443 for https) must be elided:

http://example.com      // permitted
https://example.com     // permitted
http://example.com:80   // prohibited
https://example.com:443 // prohibited

In addition to support for exact origins, this field provides limited support for origin patterns that encompass multiple origins.

When credentialed access is not enabled (i.e. when the Credentialed field is unset), a single asterisk denotes all origins:

Origins: []string{"*"},

For security reasons, specifying this origin pattern is prohibited when credentialed access is enabled:

Credentialed: true,
Origins:      []string{"*"}, // prohibited

A leading asterisk followed by a period (.) in a host pattern denotes exactly one arbitrary DNS label or several period-separated arbitrary DNS labels. For instance, the pattern

https://*.example.com

encompasses the following origins (among others):

https://foo.example.com
https://bar.example.com
https://bar.foo.example.com
https://baz.bar.foo.example.com

An asterisk in place of a port denotes an arbitrary (possibly implicit) port. For instance,

http://localhost:*

encompasses the following origins (among others):

http://localhost
http://localhost:8080
http://localhost:9090

Specifying both arbitrary subdomains and arbitrary ports in a given origin pattern is permitted. For instance,

https://*.example.com:*

encompasses the following origins (among others):

https://foo.example.com
https://foo.example.com:8080
https://foo.example.com:9090
https://bar.foo.example.com
https://bar.foo.example.com:8080
https://bar.foo.example.com:9090

No other forms of origin patterns are supported.

Origin patterns whose scheme is not https and whose host is neither localhost nor a loopback IP address are deemed insecure; as such, they are by default prohibited when credentialed access and/or some form of Private-Network Access is enabled. If, even in such cases, you deliberately wish to allow some insecure origins, you must also set the ExtraConfig.DangerouslyTolerateInsecureOrigins field.

Allowing arbitrary subdomains of a base domain that happens to be a public suffix is dangerous; as such, doing so is by default prohibited:

https://*.example.com // permitted: example.com is not a public suffix
https://*.com         // prohibited (by default): com is a public suffix
https://*.github.io   // prohibited (by default): github.io is a public suffix

If you deliberately wish to allow arbitrary subdomains of some public suffix, you must also set the ExtraConfig.DangerouslyTolerateSubdomainsOfPublicSuffixes field.

Credentialed

Credentialed, when set, configures a CORS middleware to allow credentialed access (e.g. with cookies) in addition to anonymous access.

Note that credentialed access is required only by requests that carry browser-managed credentials (as opposed to client-managed credentials, such as Bearer tokens). In practice, if you wish to allow clients to send requests that carry a header of the form

Authorization: Bearer xyz

to you server, you can likely leave Credentialed unset; instead, you should simply allow request-header name "Authorization" via the RequestHeaders field.

Methods

Methods configures a CORS middleware to allow any of the specified HTTP methods. Method names are case-sensitive.

Methods: []string{
  http.MethodGet,
  http.MethodPost,
  http.MethodPut,
  "PURGE",
}

A single asterisk denotes all methods:

Methods: []string{"*"},

The three so-called "CORS-safelisted methods" (GET, HEAD, and POST) are by default allowed by the CORS protocol. As such, allowing them explicitly in your CORS configuration is permitted but never actually necessary.

Moreover, the CORS protocol forbids the use of some method names. Accordingly, specifying forbidden method names is prohibited.

Note that, contrary to popular belief, listing OPTIONS as an allowed method in your CORS configuration is only required if you wish to allow clients to make explicit use of that method, e.g. via the following client code:

fetch('https://example.com', {method: 'OPTIONS'})

In the great majority of cases, listing OPTIONS as an allowed method in your CORS configuration is unnecessary.

RequestHeaders

RequestHeaders configures a CORS middleware to allow any of the specified request headers. Header names are case-insensitive.

RequestHeaders: []string{"Content-Type"},

When credentialed access is enabled (i.e. when the Credentialed field is set), a single asterisk denotes all request-header names:

Credentialed:   true,
RequestHeaders: []string{"*"}, // allows all request-header names

If you can, you should avoid this conjunction of enabling credentialed access and allowing all request-header names; otherwise, middleware performance may indeed suffer in the face of some adversarial preflight requests.

For both technical and security reasons, the asterisk has a different meaning when credentialed access is disabled; it then denotes all request-header names other than Authorization:

Credentialed:   false,
RequestHeaders: []string{"*"}, // allows all request-header names other than Authorization

When credentialed access is disabled, if you wish to allow Authorization in addition to all other request-header names, you must also explicitly specify that name:

Credentialed:   false,
RequestHeaders: []string{"*", "Authorization"},  // allows all request-header names

The CORS protocol defines a number of so-called "forbidden request-header names"; browsers prevent clients from including such headers in their requests. Accordingly, specifying one or more forbidden request-header names is prohibited.

Finally, some header names that have no place in a request are prohibited:

  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Private-Network
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age

MaxAgeInSeconds

MaxAgeInSeconds configures a CORS middleware to instruct browsers to cache preflight responses for a duration no longer than the specified number of seconds.

The zero value instructs browsers to cache preflight responses with a default max-age value of five seconds. To instruct browsers to eschew caching of preflight responses altogether, specify a value of -1. No other negative value is permitted.

Because modern browsers cap the max-age value (the highest cap currently is Firefox's: 86,400 seconds), this field is subject to an upper bound: specifying a value larger than 86400 is prohibited.

ResponseHeaders

ResponseHeaders configures a CORS middleware to expose the specified response headers to clients. Header names are case-insensitive.

ResponseHeaders: []string{"X-Response-Time"},

When credentialed access is disabled (i.e. when the Credentialed field is unset), a single asterisk denotes all response-header names:

ResponseHeaders: []string{"*"},

However, for technical reasons, this is only permitted if the Credentialed field is unset.

The CORS protocol defines a number of so-called "CORS-safelisted response-header names", which need not be explicitly specified as exposed. As such, explicitly specifying them as exposed in your CORS configuration is permitted but never actually necessary.

The CORS protocol also defines a number of so-called "forbidden response-header names", which cannot be exposed to clients. Accordingly, specifying one or more forbidden response-header name(s) is prohibited.

Finally, some header names that have no place in a response are prohibited:

  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Access-Control-Request-Private-Network
  • Origin

type ExtraConfig

type ExtraConfig struct {
	PreflightSuccessStatus                        int
	PrivateNetworkAccess                          bool
	PrivateNetworkAccessInNoCORSModeOnly          bool
	DangerouslyTolerateInsecureOrigins            bool
	DangerouslyTolerateSubdomainsOfPublicSuffixes bool
	// contains filtered or unexported fields
}

An ExtraConfig provides more advanced (and potentially dangerous) configuration settings.

PreflightSuccessStatus

PreflightSuccessStatus configures a CORS middleware to use the specified status code in successful preflight responses. The default status code, which is used if this field has the zero value, is 204.

Specifying a non-zero status code outside the 2xx range is prohibited.

According to the Fetch standard, any 2xx status code is acceptable to mark a prelight response as successful; however, some rare non-compliant user agents fail preflight when the preflight response has a status code other than 200 (e.g. 204). If some of your clients rely on such non-compliant user agents, you should set a custom preflight-success status of 200.

PrivateNetworkAccess

PrivateNetworkAccess configures a CORS middleware to enable Private-Network Access, which 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) and provides a server-side opt-in mechanism for allowing such access.

This setting applies to all the origins allowed in the configuration of the desired middleware.

For security reasons, PrivateNetworkAccess cannot be set when the single-asterisk origin pattern is specified in the Config.Origins field.

At most one of PrivateNetworkAccess and PrivateNetworkAccessInNoCORSModeOnly can be set.

PrivateNetworkAccessInNoCORSModeOnly

PrivateNetworkAccessInNoCORSModeOnly configures a CORS middleware to enable Private-Network Access in no-cors mode only. One use case for this setting is given in the link-shortening-service example of the Private-Network Access draft.

For security reasons, PrivateNetworkAccessInNoCORSModeOnly cannot be set when the single-asterisk origin pattern is specified in the Config.Origins field.

At most one of PrivateNetworkAccess and PrivateNetworkAccessInNoCORSModeOnly can be set.

DangerouslyTolerateInsecureOrigins

DangerouslyTolerateInsecureOrigins enables you to allow insecure origins (i.e. origins whose scheme is not https and whose host is neither localhost nor a loopback IP address), which are by default prohibited when credentialed access and/or some form of Private-Network Access is enabled.

Be aware that allowing insecure origins exposes your clients to some active network attacks, as described by James Kettle in the talk he gave at AppSec EU 2017.

DangerouslyTolerateSubdomainsOfPublicSuffixes

DangerouslyTolerateSubdomainsOfPublicSuffixes enables you to allow all subdomains of some public suffix (also known as "effective top-level domain"), which is by default prohibited.

Be aware that allowing all subdomains of a public suffix (e.g. com) is dangerous, because such domains are typically registrable by anyone, including attackers.

type Middleware

type Middleware struct {
	// contains filtered or unexported fields
}

A Middleware is a CORS middleware. Call its *Middleware.Wrap method to apply it to a http.Handler.

The zero value is ready to use but is a mere "passthrough" middleware, i.e. a middleware that simply delegates to the handler(s) it wraps. To obtain a proper CORS middleware, you should call NewMiddleware and pass it a valid Config.

Middleware have a debug mode, which can be toggled by calling their *Middleware.SetDebug method. You should turn debug mode on whenever you're struggling to troubleshoot some CORS-preflight issue; however, be aware that keeping debug mode on may lead to observably poorer middleware performance in the face of some adversarial preflight requests. When debug mode is off, the information that the middleware includes in preflight responses is minimal, for efficiency and confidentiality reasons; however, when preflight fails, the browser then lacks enough contextual information about the failure to produce a helpful CORS error message. In contrast, when debug mode is on and preflight fails, the middleware includes enough contextual information about the preflight failure in the response for browsers to produce a helpful CORS error message. The debug mode of a passthrough middleware is invariably off.

A Middleware must not be copied after first use.

Middleware are safe for concurrent use by multiple goroutines. Therefore, you are free to expose some or all of their 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.

func NewMiddleware

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

NewMiddleware creates a CORS middleware that behaves in accordance with cfg. If cfg is invalid, it returns a nil *Middleware and some non-nil error. Otherwise, it returns a pointer to a CORS Middleware and a nil error.

The debug mode of the resulting middleware is off.

Mutating the fields of cfg after NewMiddleware has returned a functioning middleware does not alter the latter's behavior. However, you can reconfigure a Middleware via its *Middleware.Reconfigure method.

If you need to programmatically handle the configuration errors constitutive of the resulting error, rely on package github.com/jub0bs/cors/cfgerrors.

func (*Middleware) Config added in v0.2.0

func (m *Middleware) Config() *Config

Config returns a pointer to a deep copy of m's current configuration; if m is a passthrough middleware, it simply returns nil. The result may differ from the Config with which m was created or last reconfigured, but the following statement is guaranteed to be a no-op (albeit a relatively expensive one):

m.Reconfigure(m.Config())

Mutating the fields of the result does not alter m's behavior. However, you can reconfigure a Middleware via its *Middleware.Reconfigure method.

func (*Middleware) Reconfigure added in v0.2.0

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

Reconfigure reconfigures m in accordance with cfg. If cfg is nil, it turns m into a passthrough middleware. If *cfg is invalid, it leaves m unchanged and returns some non-nil error. Otherwise, it successfully reconfigures m, leaves m's debug mode unchanged, and returns a nil error. The following statement is guaranteed to be a no-op (albeit a relatively expensive one):

m.Reconfigure(m.Config())

Note that

mw := new(cors.Middleware)
err := mw.Reconfigure(&cfg)

is functionally equivalent to

mw, err := cors.NewMiddleware(cfg)

You can safely reconfigure a middleware even as it's concurrently processing requests.

Mutating the fields of cfg after Reconfigure has returned does not alter m's behavior.

If you need to programmatically handle the configuration errors constitutive of the resulting error, rely on package github.com/jub0bs/cors/cfgerrors.

func (*Middleware) SetDebug

func (m *Middleware) SetDebug(b bool)

SetDebug turns debug mode on (if b is true) or off (otherwise). If m happens to be a passthrough middleware, its debug mode is invariably off and SetDebug is a no-op.

func (*Middleware) Wrap

func (m *Middleware) Wrap(h http.Handler) http.Handler

Wrap applies the CORS middleware to the specified handler.

Example
package main

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

	"github.com/jub0bs/cors"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello", handleHello) // note: not configured for CORS

	// create CORS middleware
	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)
	}

	api := http.NewServeMux()
	api.HandleFunc("GET  /users", handleUsersGet)
	api.HandleFunc("POST /users", handleUsersPost)
	mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api))) // note: method-less pattern here

	if err := http.ListenAndServe(":8080", mux); err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

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

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

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

Example (Incorrect)

The example below illustrates a common pitfall.

A good rule of thumb for avoiding this pitfall consists in registering the result of Wrap, not for a "method-full" pattern (e.g. "GET /api/dogs"), but for a "method-less" pattern; see the other example.

package main

import (
	"log"
	"net/http"

	"github.com/jub0bs/cors"
)

// The example below illustrates a common pitfall.
//
// A good rule of thumb for avoiding this pitfall consists in
// registering the result of Wrap,
// not for a "method-full" pattern (e.g. "GET /api/dogs"),
// but for a "method-less" pattern; see the other example.
func main() {
	corsMw, err := cors.NewMiddleware(cors.Config{
		Origins: []string{"https://example"},
	})
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	// Because the pattern for which the result of Wrap is registered
	// unduly specifies a method (other than OPTIONS),
	// CORS-preflight requests to /api/dogs cannot reach the CORS middleware.
	// Therefore, CORS preflight will systematically fail
	// and you'll have a bad day...
	mux.Handle("GET /api/dogs", corsMw.Wrap(http.HandlerFunc(handleDogsGet))) // incorrect!
	if err := http.ListenAndServe(":8080", mux); err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

func handleDogsGet(_ http.ResponseWriter, _ *http.Request) {
	// omitted
}
Output:

Directories

Path Synopsis
Package cfgerrors provides functionalities for programmatically handling configuration errors produced by package github.com/jub0bs/cors.
Package cfgerrors provides functionalities for programmatically handling configuration errors produced by package github.com/jub0bs/cors.
internal
headers
Package headers is all about HTTP header names.
Package headers is all about HTTP header names.
methods
Package methods is all about HTTP methods.
Package methods is all about HTTP methods.
origins
Package origin implements parsing of origins and origin patterns and provides a data structure useful for representing a set of origins.
Package origin implements parsing of origins and origin patterns and provides a data structure useful for representing a set of origins.
origins/radix
Package radix provides an implementation of a specialized radix tree.
Package radix provides an implementation of a specialized radix tree.
util
Package util provides various things used by other packages in this module.
Package util provides various things used by other packages in this module.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL