Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(middleware/cors): Handling and wildcard subdomain matching #2915

Merged
merged 34 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a27346b
fix: allow origins check
sixcolors Feb 26, 2024
058cf0a
test: AllowOrigins with whitespace
sixcolors Feb 26, 2024
9224a2e
test(middleware/cors): add benchmarks
sixcolors Feb 26, 2024
221310e
chore: fix linter errors
sixcolors Feb 26, 2024
9bc7161
test(middleware/cors): use h() instead of app.Test()
sixcolors Feb 27, 2024
000c548
test(middleware/cors): add miltiple origins in Test_CORS_AllowOriginS…
sixcolors Feb 27, 2024
b3c7a2c
chore: refactor validate and normalize
sixcolors Feb 27, 2024
a8cf4f0
test(cors/middleware): add more benchmarks
sixcolors Mar 1, 2024
bba56fb
fix(middleware/cors): handling and wildcard subdomain matching
sixcolors Mar 11, 2024
af930bd
chore: grammar
sixcolors Mar 11, 2024
03e09c9
Apply suggestions from code review
sixcolors Mar 11, 2024
6070ea3
chore: fix misspelling
sixcolors Mar 11, 2024
3e1a288
Merge branch 'fix-cors-allow-origins' of https://github.com/gofiber/f…
sixcolors Mar 11, 2024
6a482d2
test(middleware/cors): combine Invalid_Origins tests
sixcolors Mar 11, 2024
b792951
refactor(middleware/cors): headers handling
sixcolors Mar 11, 2024
5f53a50
docs(middleware/cors): Update AllowOrigins description
sixcolors Mar 12, 2024
bc32bd3
merge upstream
sixcolors Mar 14, 2024
3bc500d
chore: merge
sixcolors Mar 14, 2024
46b8d02
perf(middleware/cors): optimize handler
sixcolors Mar 14, 2024
742b915
perf(middleware/cors): optimize handler
sixcolors Mar 14, 2024
f1678ae
chore(middleware/cors): ipdate origin handling logic
sixcolors Mar 15, 2024
f12d083
chore(middleware/cors): fix header capitalization
sixcolors Mar 15, 2024
79d46e0
docs(middleware/cors): improve sercuity notes
sixcolors Mar 16, 2024
6860130
docs(middleware/cors): Improve security notes
sixcolors Mar 16, 2024
81ddb4d
docs(middleware/cors): improve CORS overview
sixcolors Mar 16, 2024
2e57b0b
docs(middleware/cors): fix ordering of how it works
sixcolors Mar 16, 2024
025d358
docs(middleware/cors): add additional info to How to works
sixcolors Mar 16, 2024
2431a09
docs(middleware/cors): rm space
sixcolors Mar 16, 2024
029c2f9
docs(middleware/cors): add validation for AllowOrigins origins to ove…
sixcolors Mar 16, 2024
d220abc
docs(middleware/cors): update ExposeHeaders and MaxAge descriptions
sixcolors Mar 16, 2024
cc8c559
docs(middleware/cors): Add dynamic origin validation example
sixcolors Mar 16, 2024
fb48678
docs(middleware/cors): Improve security notes and fix header capitali…
sixcolors Mar 16, 2024
c17860e
docs(middleware/cors): configuration examples
sixcolors Mar 16, 2024
87e7cad
docs(middleware/cors): `"*"`
sixcolors Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 50 additions & 2 deletions docs/api/middleware/cors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ CORS middleware for [Fiber](https://github.com/gofiber/fiber) that can be used t

The middleware conforms to the `access-control-allow-origin` specification by parsing `AllowOrigins`. First, the middleware checks if there is a matching allowed origin for the requesting 'origin' header. If there is a match, it returns exactly one matching domain from the list of allowed origins.

For more control, `AllowOriginsFunc` can be used to programatically determine if an origin is allowed. If no match was found in `AllowOrigins` and if `AllowOriginsFunc` returns true then the 'access-control-allow-origin' response header is set to the 'origin' request header.
For more control, `AllowOriginsFunc` can be used to programmatically determine if an origin is allowed. If no match was found in `AllowOrigins` and if `AllowOriginsFunc` returns true then the 'access-control-allow-origin' response header is set to the 'origin' request header.
sixcolors marked this conversation as resolved.
Show resolved Hide resolved

When defining your Origins make sure they are properly formatted. The middleware validates and normalizes the provided origins, ensuring they're in the correct format by checking for valid schemes (http or https), and removing any trailing slashes.

Expand Down Expand Up @@ -73,7 +73,7 @@ app.Use(cors.New(cors.Config{
|:-----------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|
| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| AllowOriginsFunc | `func(origin string) bool` | AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' response header to the 'origin' request header when returned true. This allows for dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins will be not have the 'access-control-allow-credentials' header set to 'true'. | `nil` |
| AllowOrigins | `string` | AllowOrigin defines a comma separated list of origins that may access the resource. | `"*"` |
| AllowOrigins | `string` | AllowOrigins defines a comma separated list of origins that may access the resource. This supports subdomain matching, so you can use a value like "https://.example.com" to allow any subdomain of example.com to submit requests. | `"*"` |
sixcolors marked this conversation as resolved.
Show resolved Hide resolved
| AllowMethods | `string` | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` |
sixcolors marked this conversation as resolved.
Show resolved Hide resolved
| AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` |
| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials. Note: If true, AllowOrigins cannot be set to a wildcard ("*") to prevent security vulnerabilities. | `false` |
Expand Down Expand Up @@ -101,3 +101,51 @@ var ConfigDefault = Config{
MaxAge: 0,
}
```

# How It Works

The CORS middleware works by adding the necessary CORS headers to responses from your Fiber application. These headers tell browsers what origins, methods, and headers are allowed for cross-origin requests.

When a request comes in, the middleware first checks if it's a preflight request, which is a CORS mechanism to determine whether the actual request is safe to send. Preflight requests are HTTP OPTIONS requests with specific CORS headers. If it's a preflight request, the middleware responds with the appropriate CORS headers and ends the request.

If it's not a preflight request, the middleware adds the CORS headers to the response and passes the request to the next handler. The actual CORS headers added depend on the configuration of the middleware.

The `AllowOrigins` option controls which origins can make cross-origin requests. The middleware handles different `AllowOrigins` configurations as follows:

- **Single origin:** If `AllowOrigins` is set to a single origin like `"http://www.example.com"`, and that origin matches the origin of the incoming request, the middleware adds the header `Access-Control-Allow-Origin: http://www.example.com` to the response.

- **Multiple origins:** If `AllowOrigins` is set to multiple origins like `"https://example.com, https://www.example.com"`, the middleware picks the origin that matches the origin of the incoming request.

- **Subdomain wildcard:** If `AllowOrigins` contains `"https://.example.com"`, a subdomain like `www` would be matched and `"https://www.example.com"` would be the header.

- **Wildcard origin:** If `AllowOrigins` is set to `"*"`, the middleware uses that and adds the header `Access-Control-Allow-Origin: *` to the response.

In all cases above, except the **Wildcard origin**, the middleware will either add the `Access-Control-Allow-Origin` header to the response matching the origin of the incoming request, or it will not add the header at all if the origin is not allowed.

The `AllowMethods` option controls which HTTP methods are allowed. For example, if `AllowMethods` is set to `"GET, POST"`, the middleware adds the header `Access-Control-Allow-Methods: GET, POST` to the response.

The middleware also handles the `AllowOriginsFunc` option, which allows you to programmatically determine if an origin is allowed. If `AllowOriginsFunc` returns `true` for an origin, the middleware sets the `Access-Control-Allow-Origin` header to that origin.

This way, the CORS middleware allows you to control how your Fiber application responds to cross-origin requests.

## Security Considerations

When configuring CORS, misconfiguration can potentially expose your application to various security risks.

- **Allowing all origins:** Setting `Access-Control-Allow-Origin` to `*` (a wildcard) allows any domain to make cross-origin requests. This can expose your application to cross-site request forgery (CSRF) attacks. It's generally safer to specify the exact domains allowed to make requests.

- **Allowing credentials:** The `Access-Control-Allow-Credentials` header indicates whether the browser should include credentials with cross-origin requests. If this is set to `true`, it can expose your application to attacks if combined with a wildcard `Access-Control-Allow-Origin`. We specifically prohibit this action in our CORS middleware, in line with the Fetch specification.

- **Exposing headers:** The `Access-Control-Expose-Headers` header lets the server whitelist headers that browsers are allowed to access. Be careful not to expose sensitive headers.

:::note
In our CORS middleware, we specifically prevent `Access-Control-Allow-Credentials` from being `true` when `Access-Control-Allow-Origin` is set to the wildcard (`*`). This prevents potential security risks associated with allowing credentials to be shared with all origins.

When using `AllowOrigins`, a configuration check will cause a panic if `Access-Control-Allow-Credentials` is `true` and `Access-Control-Allow-Origin` is set to the wildcard.
:::

:::caution
Be extra careful when using `AllowOriginsFunc`. Make sure to properly validate the origin to prevent potential security risks.

When using `AllowOriginsFunc`, the `Access-Control-Allow-Origin` header will always be set to the origin header if the func returns `true`, which can bypass such protections if you return `true` in all situations.
sixcolors marked this conversation as resolved.
Show resolved Hide resolved
:::
140 changes: 84 additions & 56 deletions middleware/cors/cors.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,31 @@ func New(config ...Config) fiber.Handler {

// allowOrigins is a slice of strings that contains the allowed origins
// defined in the 'AllowOrigins' configuration.
var allowOrigins []string
allowOrigins := []string{}
allowSOrigins := []subdomain{}
allowAllOrigins := false

// Validate and normalize static AllowOrigins
if cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" {
origins := strings.Split(cfg.AllowOrigins, ",")
allowOrigins = make([]string, len(origins))

for i, origin := range origins {
for _, origin := range origins {
trimmedOrigin := strings.TrimSpace(origin)
isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin)

if isValid {
allowOrigins[i] = normalizedOrigin
} else {
if !isValid {
log.Warnf("[CORS] Invalid origin format in configuration: %s", trimmedOrigin)
panic("[CORS] Invalid origin provided in configuration")
}

if i := strings.Index(normalizedOrigin, "://."); i != -1 {
allowSOrigins = append(allowSOrigins, subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]})
} else {
allowOrigins = append(allowOrigins, normalizedOrigin)
}
}
} else {
// If AllowOrigins is set to a wildcard or not set,
// set allowOrigins to a slice with a single element
allowOrigins = []string{cfg.AllowOrigins}
} else if cfg.AllowOrigins == "*" {
allowAllOrigins = true
}

// Strip white spaces
Expand All @@ -155,18 +158,36 @@ func New(config ...Config) fiber.Handler {
}

// Get originHeader header
originHeader := c.Get(fiber.HeaderOrigin)
originHeader := strings.ToLower(c.Get(fiber.HeaderOrigin))

// If the request does not have an Origin header, the request is outside the scope of CORS
if originHeader == "" {
return c.Next()
}

// Set default allowOrigin to empty string
allowOrigin := ""

// Check allowed origins
for _, origin := range allowOrigins {
if origin == "*" {
allowOrigin = "*"
break
if allowAllOrigins {
allowOrigin = "*"
} else {
// Check if the origin is in the list of allowed origins
for _, origin := range allowOrigins {
if origin == originHeader {
allowOrigin = originHeader
break
}
}
if validateDomain(originHeader, origin) {
allowOrigin = originHeader
break

// Check if the origin is in the list of allowed subdomains
if allowOrigin == "" {
for _, sOrigin := range allowSOrigins {
if sOrigin.match(originHeader) {
allowOrigin = originHeader
break
}
}
}
}

Expand All @@ -179,56 +200,63 @@ func New(config ...Config) fiber.Handler {

// Simple request
if c.Method() != fiber.MethodOptions {
c.Vary(fiber.HeaderOrigin)
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)

if cfg.AllowCredentials {
c.Set(fiber.HeaderAccessControlAllowCredentials, "true")
}
if exposeHeaders != "" {
c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders)
}
setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg)
return c.Next()
}

// Preflight request
c.Vary(fiber.HeaderOrigin)
c.Vary(fiber.HeaderAccessControlRequestMethod)
c.Vary(fiber.HeaderAccessControlRequestHeaders)
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)
c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods)

if cfg.AllowCredentials {
// When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*'
if allowOrigin != "*" && allowOrigin != "" {
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)
c.Set(fiber.HeaderAccessControlAllowCredentials, "true")
} else if allowOrigin == "*" {
log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.")
}
} else {
// For non-credential requests, it's safe to set to '*' or specific origins
setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg)

// Send 204 No Content
return c.SendStatus(fiber.StatusNoContent)
}
}

// Function to set CORS headers
func setCORSHeaders(c *fiber.Ctx, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge string, cfg Config) {
c.Vary(fiber.HeaderOrigin)

if cfg.AllowCredentials {
// When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*'
if allowOrigin != "*" && allowOrigin != "" {
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)
c.Set(fiber.HeaderAccessControlAllowCredentials, "true")
} else if allowOrigin == "*" {
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)
log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.")
}
} else if len(allowOrigin) > 0 {
// For non-credential requests, it's safe to set to '*' or specific origins
c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)
}

// Set Allow-Headers if not empty
if allowHeaders != "" {
c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders)
} else {
h := c.Get(fiber.HeaderAccessControlRequestHeaders)
if h != "" {
c.Set(fiber.HeaderAccessControlAllowHeaders, h)
}
}
// Set Allow-Methods if not empty
if allowMethods != "" {
c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods)
}

// Set MaxAge is set
if cfg.MaxAge > 0 {
c.Set(fiber.HeaderAccessControlMaxAge, maxAge)
} else if cfg.MaxAge < 0 {
c.Set(fiber.HeaderAccessControlMaxAge, "0")
// Set Allow-Headers if not empty
if allowHeaders != "" {
c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders)
} else {
h := c.Get(fiber.HeaderAccessControlRequestHeaders)
if h != "" {
c.Set(fiber.HeaderAccessControlAllowHeaders, h)
}
}

// Send 204 No Content
return c.SendStatus(fiber.StatusNoContent)
// Set MaxAge if set
if cfg.MaxAge > 0 {
c.Set(fiber.HeaderAccessControlMaxAge, maxAge)
} else if cfg.MaxAge < 0 {
c.Set(fiber.HeaderAccessControlMaxAge, "0")
}

// Set Expose-Headers if not empty
if exposeHeaders != "" {
c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders)
}
}