cors

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 8, 2024 License: MIT Imports: 11 Imported by: 1

README

jub0bs/cors

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.

This package allows you to configure and build net/http middleware that implement CORS.

Installation

go get github.com/jub0bs/cors

jub0bs/cors requires Go 1.22 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, and
  • (optionally) with request header Authorization,

and how to apply the middleware in question to all the resources accessible under some /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

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

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

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

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

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.

Documentation

The documentation is available on pkg.go.dev.

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

Code coverage

coverage

Benchmarks

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

License

All source code is covered by the MIT License.

Additional resources

Reasons for favoring rs/cors over jub0bs/cors

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

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 reverse proxies, 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.
  • The CORS response headers that are set by this library's middleware MUST NOT be altered; moreover, additional CORS response headers MUST NOT be included in responses.
  • The Vary headers that are set by this library's middleware SHOULD NOT be altered; however, additional Vary headers MAY be included in responses.
  • 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).

Permitted schemes are limited to http (with one caveat explained further down) and https; specifying origin patterns with other schemes is prohibited:

http://example.com             // permitted
https://example.com            // permitted
chrome-extension://example.com // 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

Specifying origin patterns in addition to the single-asterisk origin pattern is prohibited:

Origins: []string{"*", "https://example.com"}, // 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:80
http://localhost:9090

Specifying both arbitrary subdomains and arbitrary ports in a given origin pattern is prohibited:

https://*.example.com      // permitted
https://*.example.com:9090 // permitted
https://example.com:*      // permitted
https://*.example.com:*    // prohibited

No other forms of origin patterns are supported.

Origin patterns whose scheme is http 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{"*"},

Specifying methods in addition to the asterisk is prohibited:

Methods: []string{"*", "POST"}, // prohibited

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

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

Specifying request-header names other than Authorization in addition to the asterisk is prohibited:

RequestHeaders: []string{"*", "Foo"},                  // prohibited
RequestHeaders: []string{"*", "Authorization", "Foo"}, // prohibited

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.

Specifying response-header names in addition to the asterisk is prohibited:

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

The CORS protocol defines a number of so-called "CORS-safelisted response-header names", which need not be explicitly specified as exposed. 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 safelisted or 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.

Setting a custom preflight-success status is useful when some of your clients choke on preflight responses that are meant to be successful but have a 2xx status code other than 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 http), 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. 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 just 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.

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

func (*Middleware) Config added in v0.2.0

func (m *Middleware) Config() *Config

Config returns a 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.

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.

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

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

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

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

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
	// omitted
}
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!
	log.Fatal(http.ListenAndServe(":8080", mux))
}

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

Directories

Path Synopsis
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