2024-03-09 11:21:24 +02:00
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
2016-04-08 01:16:58 +02:00
package middleware
import (
"net/http"
2020-08-18 03:39:54 +02:00
"regexp"
2016-04-08 01:16:58 +02:00
"strconv"
"strings"
2019-01-30 12:56:56 +02:00
"github.com/labstack/echo/v4"
2016-04-08 01:16:58 +02:00
)
2024-03-09 17:53:07 +02:00
// CORSConfig defines the config for CORS middleware.
type CORSConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
2016-07-27 18:34:44 +02:00
2024-03-09 17:53:07 +02:00
// AllowOrigins determines the value of the Access-Control-Allow-Origin
// response header. This header defines a list of origins that may access the
// resource. The wildcard characters '*' and '?' are supported and are
// converted to regex fragments '.*' and '.' accordingly.
//
// Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// Optional. Default value []string{"*"}.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
AllowOrigins [ ] string ` yaml:"allow_origins" `
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// AllowOriginFunc is a custom function to validate the origin. It takes the
// origin as an argument and returns true if allowed or false otherwise. If
// an error is returned, it is returned by the handler. If this option is
// set, AllowOrigins is ignored.
//
// Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// Optional.
AllowOriginFunc func ( origin string ) ( bool , error ) ` yaml:"-" `
2020-10-09 11:07:29 +02:00
2024-03-09 17:53:07 +02:00
// AllowMethods determines the value of the Access-Control-Allow-Methods
// response header. This header specified the list of methods allowed when
// accessing the resource. This is used in response to a preflight request.
//
// Optional. Default value DefaultCORSConfig.AllowMethods.
// If `allowMethods` is left empty, this middleware will fill for preflight
// request `Access-Control-Allow-Methods` header value
// from `Allow` header that echo.Router set into context.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
AllowMethods [ ] string ` yaml:"allow_methods" `
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// AllowHeaders determines the value of the Access-Control-Allow-Headers
// response header. This header is used in response to a preflight request to
// indicate which HTTP headers can be used when making the actual request.
//
// Optional. Default value []string{}.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
AllowHeaders [ ] string ` yaml:"allow_headers" `
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// AllowCredentials determines the value of the
// Access-Control-Allow-Credentials response header. This header indicates
// whether or not the response to the request can be exposed when the
// credentials mode (Request.credentials) 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. See also
// [MDN: Access-Control-Allow-Credentials].
//
// Optional. Default value false, in which case the header is not set.
//
// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
// See "Exploiting CORS misconfigurations for Bitcoins and bounties",
// https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
AllowCredentials bool ` yaml:"allow_credentials" `
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
//
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
//
// Optional. Default value is false.
UnsafeWildcardOriginWithAllowCredentials bool ` yaml:"unsafe_wildcard_origin_with_allow_credentials" `
2023-02-21 12:21:49 +02:00
2024-03-09 17:53:07 +02:00
// ExposeHeaders determines the value of Access-Control-Expose-Headers, which
// defines a list of headers that clients are allowed to access.
//
// Optional. Default value []string{}, in which case the header is not set.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header
ExposeHeaders [ ] string ` yaml:"expose_headers" `
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// MaxAge determines the value of the Access-Control-Max-Age response header.
// This header indicates how long (in seconds) the results of a preflight
// request can be cached.
// The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response.
//
// Optional. Default value 0 - meaning header is not sent.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
MaxAge int ` yaml:"max_age" `
}
2016-04-08 01:16:58 +02:00
2024-03-09 17:53:07 +02:00
// DefaultCORSConfig is the default CORS middleware config.
var DefaultCORSConfig = CORSConfig {
Skipper : DefaultSkipper ,
AllowOrigins : [ ] string { "*" } ,
AllowMethods : [ ] string { http . MethodGet , http . MethodHead , http . MethodPut , http . MethodPatch , http . MethodPost , http . MethodDelete } ,
}
2016-04-08 01:16:58 +02:00
2016-04-08 01:57:57 +02:00
// CORS returns a Cross-Origin Resource Sharing (CORS) middleware.
2022-09-12 20:53:44 +02:00
// See also [MDN: Cross-Origin Resource Sharing (CORS)].
//
// Security: Poorly configured CORS can compromise security because it allows
// relaxation of the browser's Same-Origin policy. See [Exploiting CORS
// misconfigurations for Bitcoins and bounties] and [Portswigger: Cross-origin
// resource sharing (CORS)] for more details.
//
// [MDN: Cross-Origin Resource Sharing (CORS)]: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS
// [Exploiting CORS misconfigurations for Bitcoins and bounties]: https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
// [Portswigger: Cross-origin resource sharing (CORS)]: https://portswigger.net/web-security/cors
2016-04-08 01:16:58 +02:00
func CORS ( ) echo . MiddlewareFunc {
2016-04-08 06:20:50 +02:00
return CORSWithConfig ( DefaultCORSConfig )
2016-04-08 01:16:58 +02:00
}
2016-09-01 05:10:14 +02:00
// CORSWithConfig returns a CORS middleware with config.
2022-09-12 20:53:44 +02:00
// See: [CORS].
2016-04-08 06:20:50 +02:00
func CORSWithConfig ( config CORSConfig ) echo . MiddlewareFunc {
2016-04-08 01:16:58 +02:00
// Defaults
2016-07-27 18:34:44 +02:00
if config . Skipper == nil {
config . Skipper = DefaultCORSConfig . Skipper
}
2016-11-22 00:42:13 +02:00
if len ( config . AllowOrigins ) == 0 {
config . AllowOrigins = DefaultCORSConfig . AllowOrigins
}
2021-12-04 20:02:11 +02:00
hasCustomAllowMethods := true
2016-04-08 01:16:58 +02:00
if len ( config . AllowMethods ) == 0 {
2021-12-04 20:02:11 +02:00
hasCustomAllowMethods = false
2016-04-08 01:16:58 +02:00
config . AllowMethods = DefaultCORSConfig . AllowMethods
}
2016-11-13 06:24:53 +02:00
2020-08-18 03:39:54 +02:00
allowOriginPatterns := [ ] string { }
for _ , origin := range config . AllowOrigins {
pattern := regexp . QuoteMeta ( origin )
2023-03-28 10:42:55 +02:00
pattern = strings . ReplaceAll ( pattern , "\\*" , ".*" )
pattern = strings . ReplaceAll ( pattern , "\\?" , "." )
2020-08-18 03:39:54 +02:00
pattern = "^" + pattern + "$"
allowOriginPatterns = append ( allowOriginPatterns , pattern )
}
2016-04-08 01:16:58 +02:00
allowMethods := strings . Join ( config . AllowMethods , "," )
allowHeaders := strings . Join ( config . AllowHeaders , "," )
exposeHeaders := strings . Join ( config . ExposeHeaders , "," )
2023-09-19 07:24:47 +02:00
maxAge := "0"
if config . MaxAge > 0 {
maxAge = strconv . Itoa ( config . MaxAge )
}
2016-04-08 01:16:58 +02:00
return func ( next echo . HandlerFunc ) echo . HandlerFunc {
return func ( c echo . Context ) error {
2016-07-27 18:34:44 +02:00
if config . Skipper ( c ) {
return next ( c )
}
2016-04-24 19:21:23 +02:00
req := c . Request ( )
2016-05-03 17:32:28 +02:00
res := c . Response ( )
2016-11-13 06:24:53 +02:00
origin := req . Header . Get ( echo . HeaderOrigin )
2016-11-22 00:42:13 +02:00
allowOrigin := ""
2016-11-13 06:24:53 +02:00
2020-11-06 02:15:40 +02:00
res . Header ( ) . Add ( echo . HeaderVary , echo . HeaderOrigin )
2021-12-04 20:02:11 +02:00
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
// For simplicity we just consider method type and later `Origin` header.
preflight := req . Method == http . MethodOptions
// Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware
// as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth
// middlewares by calling next(c).
// But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default
// handler does.
routerAllowMethods := ""
if preflight {
tmpAllowMethods , ok := c . Get ( echo . ContextKeyHeaderAllow ) . ( string )
if ok && tmpAllowMethods != "" {
routerAllowMethods = tmpAllowMethods
c . Response ( ) . Header ( ) . Set ( echo . HeaderAllow , routerAllowMethods )
}
}
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
2020-11-06 02:15:40 +02:00
if origin == "" {
if ! preflight {
return next ( c )
}
return c . NoContent ( http . StatusNoContent )
}
2020-11-16 05:53:49 +02:00
if config . AllowOriginFunc != nil {
allowed , err := config . AllowOriginFunc ( origin )
if err != nil {
return err
}
if allowed {
allowOrigin = origin
}
} else {
2020-10-09 11:07:29 +02:00
// Check allowed origins
for _ , o := range config . AllowOrigins {
2023-02-21 12:21:49 +02:00
if o == "*" && config . AllowCredentials && config . UnsafeWildcardOriginWithAllowCredentials {
2020-10-09 11:07:29 +02:00
allowOrigin = origin
break
2020-08-18 03:39:54 +02:00
}
2020-10-09 11:07:29 +02:00
if o == "*" || o == origin {
allowOrigin = o
2020-08-18 03:39:54 +02:00
break
}
2020-10-09 11:07:29 +02:00
if matchSubdomain ( origin , o ) {
2020-08-18 03:39:54 +02:00
allowOrigin = origin
break
}
}
2020-10-09 11:07:29 +02:00
2021-12-04 20:02:11 +02:00
checkPatterns := false
if allowOrigin == "" {
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
2021-12-09 21:57:20 +02:00
if len ( origin ) <= ( 253 + 3 + 5 ) && strings . Contains ( origin , "://" ) {
2021-12-04 20:02:11 +02:00
checkPatterns = true
}
}
if checkPatterns {
for _ , re := range allowOriginPatterns {
2020-10-09 11:07:29 +02:00
if match , _ := regexp . MatchString ( re , origin ) ; match {
allowOrigin = origin
break
}
}
}
2020-08-18 03:39:54 +02:00
}
2020-11-06 02:15:40 +02:00
// Origin not allowed
if allowOrigin == "" {
if ! preflight {
return next ( c )
}
return c . NoContent ( http . StatusNoContent )
}
2021-12-04 20:02:11 +02:00
res . Header ( ) . Set ( echo . HeaderAccessControlAllowOrigin , allowOrigin )
if config . AllowCredentials {
res . Header ( ) . Set ( echo . HeaderAccessControlAllowCredentials , "true" )
}
2016-04-08 01:16:58 +02:00
// Simple request
2020-11-06 02:15:40 +02:00
if ! preflight {
2016-04-08 01:16:58 +02:00
if exposeHeaders != "" {
2016-05-03 17:32:28 +02:00
res . Header ( ) . Set ( echo . HeaderAccessControlExposeHeaders , exposeHeaders )
2016-04-08 01:16:58 +02:00
}
return next ( c )
}
// Preflight request
2016-05-03 17:32:28 +02:00
res . Header ( ) . Add ( echo . HeaderVary , echo . HeaderAccessControlRequestMethod )
res . Header ( ) . Add ( echo . HeaderVary , echo . HeaderAccessControlRequestHeaders )
2021-12-04 20:02:11 +02:00
if ! hasCustomAllowMethods && routerAllowMethods != "" {
res . Header ( ) . Set ( echo . HeaderAccessControlAllowMethods , routerAllowMethods )
} else {
res . Header ( ) . Set ( echo . HeaderAccessControlAllowMethods , allowMethods )
2016-04-08 01:16:58 +02:00
}
2021-12-04 20:02:11 +02:00
2016-04-08 01:16:58 +02:00
if allowHeaders != "" {
2016-05-03 17:32:28 +02:00
res . Header ( ) . Set ( echo . HeaderAccessControlAllowHeaders , allowHeaders )
2016-04-08 01:16:58 +02:00
} else {
2016-09-23 07:53:44 +02:00
h := req . Header . Get ( echo . HeaderAccessControlRequestHeaders )
2016-04-08 01:16:58 +02:00
if h != "" {
2016-05-03 17:32:28 +02:00
res . Header ( ) . Set ( echo . HeaderAccessControlAllowHeaders , h )
2016-04-08 01:16:58 +02:00
}
}
2023-09-19 07:24:47 +02:00
if config . MaxAge != 0 {
2016-05-03 17:32:28 +02:00
res . Header ( ) . Set ( echo . HeaderAccessControlMaxAge , maxAge )
2016-04-08 01:16:58 +02:00
}
return c . NoContent ( http . StatusNoContent )
}
}
}