From 92ced8cf20675a803727ee9e90417eb1873dd9d8 Mon Sep 17 00:00:00 2001 From: Roman Perekhod Date: Wed, 3 Jul 2024 12:59:14 +0200 Subject: [PATCH] fix the cors vulnerability alert --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/rs/cors/README.md | 26 ++-- vendor/github.com/rs/cors/cors.go | 91 ++++++-------- .../github.com/rs/cors/internal/sortedset.go | 113 ++++++++++++++++++ vendor/github.com/rs/cors/utils.go | 66 +++------- vendor/modules.txt | 3 +- 7 files changed, 186 insertions(+), 119 deletions(-) create mode 100644 vendor/github.com/rs/cors/internal/sortedset.go diff --git a/go.mod b/go.mod index 0812a3daf67..3b03ea9504e 100644 --- a/go.mod +++ b/go.mod @@ -293,7 +293,7 @@ require ( github.com/prometheus/statsd_exporter v0.22.8 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rivo/uniseg v0.4.2 // indirect - github.com/rs/cors v1.10.1 // indirect + github.com/rs/cors v1.11.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 7905cb506ce..6bf4581ddb4 100644 --- a/go.sum +++ b/go.sum @@ -1909,8 +1909,8 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= -github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= diff --git a/vendor/github.com/rs/cors/README.md b/vendor/github.com/rs/cors/README.md index fe12fa671d1..c7fbea00348 100644 --- a/vendor/github.com/rs/cors/README.md +++ b/vendor/github.com/rs/cors/README.md @@ -88,11 +88,14 @@ handler = c.Handler(handler) * **AllowedOrigins** `[]string`: A list of origins a cross-domain request can be executed from. If the special `*` value is present in the list, all origins will be allowed. An origin may contain a wildcard (`*`) to replace 0 or more characters (i.e.: `http://*.domain.com`). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin. The default value is `*`. * **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It takes the origin as an argument and returns true if allowed, or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored. -* **AllowOriginRequestFunc** `func (r *http.Request, origin string) bool`: A custom function to validate the origin. It takes the HTTP Request object and the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` and `AllowOriginFunc` is ignored +* **AllowOriginRequestFunc** `func (r *http.Request, origin string) bool`: A custom function to validate the origin. It takes the HTTP Request object and the origin as argument and returns true if allowed or false otherwise. If this option is set, the contents of `AllowedOrigins` and `AllowOriginFunc` are ignored. +Deprecated: use `AllowOriginVaryRequestFunc` instead. +* **AllowOriginVaryRequestFunc** `func(r *http.Request, origin string) (bool, []string)`: A custom function to validate the origin. It takes the HTTP Request object and the origin as argument and returns true if allowed or false otherwise with a list of headers used to take that decision if any so they can be added to the Vary header. If this option is set, the contents of `AllowedOrigins`, `AllowOriginFunc` and `AllowOriginRequestFunc` are ignored. * **AllowedMethods** `[]string`: A list of methods the client is allowed to use with cross-domain requests. Default value is simple methods (`GET` and `POST`). * **AllowedHeaders** `[]string`: A list of non simple headers the client is allowed to use with cross-domain requests. -* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification +* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification. * **AllowCredentials** `bool`: Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. The default is `false`. +* **AllowPrivateNetwork** `bool`: Indicates whether to accept cross-origin requests over a private network. * **MaxAge** `int`: Indicates how long (in seconds) the results of a preflight request can be cached. The default is `0` which stands for no max age. * **OptionsPassthrough** `bool`: Instructs preflight to let other potential next handlers to process the `OPTIONS` method. Turn this on if your application handles `OPTIONS`. * **OptionsSuccessStatus** `int`: Provides a status code to use for successful OPTIONS requests. Default value is `http.StatusNoContent` (`204`). @@ -102,18 +105,21 @@ See [API documentation](http://godoc.org/github.com/rs/cors) for more info. ## Benchmarks +``` goos: darwin goarch: arm64 pkg: github.com/rs/cors -BenchmarkWithout-10 352671961 3.317 ns/op 0 B/op 0 allocs/op -BenchmarkDefault-10 18261723 65.63 ns/op 0 B/op 0 allocs/op -BenchmarkAllowedOrigin-10 13309591 90.21 ns/op 0 B/op 0 allocs/op -BenchmarkPreflight-10 7247728 166.9 ns/op 0 B/op 0 allocs/op -BenchmarkPreflightHeader-10 5915437 202.9 ns/op 0 B/op 0 allocs/op -BenchmarkWildcard/match-10 250336476 4.784 ns/op 0 B/op 0 allocs/op -BenchmarkWildcard/too_short-10 1000000000 0.6354 ns/op 0 B/op 0 allocs/op +BenchmarkWithout-10 135325480 8.124 ns/op 0 B/op 0 allocs/op +BenchmarkDefault-10 24082140 51.40 ns/op 0 B/op 0 allocs/op +BenchmarkAllowedOrigin-10 16424518 88.25 ns/op 0 B/op 0 allocs/op +BenchmarkPreflight-10 8010259 147.3 ns/op 0 B/op 0 allocs/op +BenchmarkPreflightHeader-10 6850962 175.0 ns/op 0 B/op 0 allocs/op +BenchmarkWildcard/match-10 253275342 4.714 ns/op 0 B/op 0 allocs/op +BenchmarkWildcard/too_short-10 1000000000 0.6235 ns/op 0 B/op 0 allocs/op PASS -ok github.com/rs/cors 9.613s +ok github.com/rs/cors 99.131s +``` + ## Licenses All source code is licensed under the [MIT License](https://raw.github.com/rs/cors/master/LICENSE). diff --git a/vendor/github.com/rs/cors/cors.go b/vendor/github.com/rs/cors/cors.go index 69f89d8f94c..da80d343b3d 100644 --- a/vendor/github.com/rs/cors/cors.go +++ b/vendor/github.com/rs/cors/cors.go @@ -26,6 +26,8 @@ import ( "os" "strconv" "strings" + + "github.com/rs/cors/internal" ) var headerVaryOrigin = []string{"Origin"} @@ -49,13 +51,15 @@ type Options struct { // takes the HTTP Request object and the origin as argument and returns true // if allowed or false otherwise. If headers are used take the decision, // consider using AllowOriginVaryRequestFunc instead. If this option is set, - // the content of `AllowedOrigins`, `AllowOriginFunc` are ignored. + // the contents of `AllowedOrigins`, `AllowOriginFunc` are ignored. + // + // Deprecated: use `AllowOriginVaryRequestFunc` instead. AllowOriginRequestFunc func(r *http.Request, origin string) bool // AllowOriginVaryRequestFunc is a custom function to validate the origin. // It takes the HTTP Request object and the origin as argument and returns // true if allowed or false otherwise with a list of headers used to take // that decision if any so they can be added to the Vary header. If this - // option is set, the content of `AllowedOrigins`, `AllowOriginFunc` and + // option is set, the contents of `AllowedOrigins`, `AllowOriginFunc` and // `AllowOriginRequestFunc` are ignored. AllowOriginVaryRequestFunc func(r *http.Request, origin string) (bool, []string) // AllowedMethods is a list of methods the client is allowed to use with @@ -109,7 +113,11 @@ type Cors struct { // Optional origin validator function allowOriginFunc func(r *http.Request, origin string) (bool, []string) // Normalized list of allowed headers - allowedHeaders []string + // Note: the Fetch standard guarantees that CORS-unsafe request-header names + // (i.e. the values listed in the Access-Control-Request-Headers header) + // are unique and sorted; + // see https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names. + allowedHeaders internal.SortedSet // Normalized list of allowed methods allowedMethods []string // Pre-computed normalized list of exposed headers @@ -140,33 +148,29 @@ func New(options Options) *Cors { c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) } - if options.AllowOriginVaryRequestFunc != nil { + // Allowed origins + switch { + case options.AllowOriginVaryRequestFunc != nil: c.allowOriginFunc = options.AllowOriginVaryRequestFunc - } else if options.AllowOriginRequestFunc != nil { + case options.AllowOriginRequestFunc != nil: c.allowOriginFunc = func(r *http.Request, origin string) (bool, []string) { return options.AllowOriginRequestFunc(r, origin), nil } - } else if options.AllowOriginFunc != nil { + case options.AllowOriginFunc != nil: c.allowOriginFunc = func(r *http.Request, origin string) (bool, []string) { return options.AllowOriginFunc(origin), nil } - } - - // Normalize options - // Note: for origins matching, the spec requires a case-sensitive matching. - // As it may error prone, we chose to ignore the spec here. - - // Allowed Origins - if len(options.AllowedOrigins) == 0 { + case len(options.AllowedOrigins) == 0: if c.allowOriginFunc == nil { // Default is all origins c.allowedOriginsAll = true } - } else { + default: c.allowedOrigins = []string{} c.allowedWOrigins = []wildcard{} for _, origin := range options.AllowedOrigins { - // Normalize + // Note: for origins matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. origin = strings.ToLower(origin) if origin == "*" { // If "*" is present in the list, turn the whole list into a match all @@ -185,15 +189,19 @@ func New(options Options) *Cors { } // Allowed Headers + // Note: the Fetch standard guarantees that CORS-unsafe request-header names + // (i.e. the values listed in the Access-Control-Request-Headers header) + // are lowercase; see https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names. if len(options.AllowedHeaders) == 0 { // Use sensible defaults - c.allowedHeaders = []string{"Accept", "Content-Type", "X-Requested-With"} + c.allowedHeaders = internal.NewSortedSet("accept", "content-type", "x-requested-with") } else { - c.allowedHeaders = convert(options.AllowedHeaders, http.CanonicalHeaderKey) + normalized := convert(options.AllowedHeaders, strings.ToLower) + c.allowedHeaders = internal.NewSortedSet(normalized...) for _, h := range options.AllowedHeaders { if h == "*" { c.allowedHeadersAll = true - c.allowedHeaders = nil + c.allowedHeaders = internal.SortedSet{} break } } @@ -353,10 +361,12 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { c.logf(" Preflight aborted: method '%s' not allowed", reqMethod) return } - reqHeadersRaw := r.Header["Access-Control-Request-Headers"] - reqHeaders, reqHeadersEdited := convertDidCopy(splitHeaderValues(reqHeadersRaw), http.CanonicalHeaderKey) - if !c.areHeadersAllowed(reqHeaders) { - c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders) + // Note: the Fetch standard guarantees that at most one + // Access-Control-Request-Headers header is present in the preflight request; + // see step 5.2 in https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. + reqHeaders, found := first(r.Header, "Access-Control-Request-Headers") + if found && !c.allowedHeadersAll && !c.allowedHeaders.Subsumes(reqHeaders[0]) { + c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders[0]) return } if c.allowedOriginsAll { @@ -367,14 +377,10 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { // Spec says: Since the list of methods can be unbounded, simply returning the method indicated // by Access-Control-Request-Method (if supported) can be enough headers["Access-Control-Allow-Methods"] = r.Header["Access-Control-Request-Method"] - if len(reqHeaders) > 0 { + if found && len(reqHeaders[0]) > 0 { // Spec says: Since the list of headers can be unbounded, simply returning supported headers // from Access-Control-Request-Headers can be enough - if reqHeadersEdited || len(reqHeaders) != len(reqHeadersRaw) { - headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) - } else { - headers["Access-Control-Allow-Headers"] = reqHeadersRaw - } + headers["Access-Control-Allow-Headers"] = reqHeaders } if c.allowCredentials { headers["Access-Control-Allow-Credentials"] = headerTrue @@ -398,10 +404,10 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { allowed, additionalVaryHeaders := c.isOriginAllowed(r, origin) // Always set Vary, see https://github.com/rs/cors/issues/10 - if vary, found := headers["Vary"]; found { - headers["Vary"] = append(vary, headerVaryOrigin[0]) - } else { + if vary := headers["Vary"]; vary == nil { headers["Vary"] = headerVaryOrigin + } else { + headers["Vary"] = append(vary, headerVaryOrigin[0]) } if len(additionalVaryHeaders) > 0 { headers.Add("Vary", strings.Join(convert(additionalVaryHeaders, http.CanonicalHeaderKey), ", ")) @@ -494,24 +500,3 @@ func (c *Cors) isMethodAllowed(method string) bool { } return false } - -// areHeadersAllowed checks if a given list of headers are allowed to used within -// a cross-domain request. -func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { - if c.allowedHeadersAll || len(requestedHeaders) == 0 { - return true - } - for _, header := range requestedHeaders { - found := false - for _, h := range c.allowedHeaders { - if h == header { - found = true - break - } - } - if !found { - return false - } - } - return true -} diff --git a/vendor/github.com/rs/cors/internal/sortedset.go b/vendor/github.com/rs/cors/internal/sortedset.go new file mode 100644 index 00000000000..513da20f7d0 --- /dev/null +++ b/vendor/github.com/rs/cors/internal/sortedset.go @@ -0,0 +1,113 @@ +// adapted from github.com/jub0bs/cors +package internal + +import ( + "sort" + "strings" +) + +// A SortedSet represents a mathematical set of strings sorted in +// lexicographical order. +// Each element has a unique position ranging from 0 (inclusive) +// to the set's cardinality (exclusive). +// The zero value represents an empty set. +type SortedSet struct { + m map[string]int + maxLen int +} + +// NewSortedSet returns a SortedSet that contains all of elems, +// but no other elements. +func NewSortedSet(elems ...string) SortedSet { + sort.Strings(elems) + m := make(map[string]int) + var maxLen int + i := 0 + for _, s := range elems { + if _, exists := m[s]; exists { + continue + } + m[s] = i + i++ + maxLen = max(maxLen, len(s)) + } + return SortedSet{ + m: m, + maxLen: maxLen, + } +} + +// Size returns the cardinality of set. +func (set SortedSet) Size() int { + return len(set.m) +} + +// String sorts joins the elements of set (in lexicographical order) +// with a comma and returns the resulting string. +func (set SortedSet) String() string { + elems := make([]string, len(set.m)) + for elem, i := range set.m { + elems[i] = elem // safe indexing, by construction of SortedSet + } + return strings.Join(elems, ",") +} + +// Subsumes reports whether csv is a sequence of comma-separated names that are +// - all elements of set, +// - sorted in lexicographically order, +// - unique. +func (set SortedSet) Subsumes(csv string) bool { + if csv == "" { + return true + } + posOfLastNameSeen := -1 + chunkSize := set.maxLen + 1 // (to accommodate for at least one comma) + for { + // As a defense against maliciously long names in csv, + // we only process at most chunkSize bytes per iteration. + end := min(len(csv), chunkSize) + comma := strings.IndexByte(csv[:end], ',') + var name string + if comma == -1 { + name = csv + } else { + name = csv[:comma] + } + pos, found := set.m[name] + if !found { + return false + } + // The names in csv are expected to be sorted in lexicographical order + // and appear at most once in csv. + // Therefore, the positions (in set) of the names that + // appear in csv should form a strictly increasing sequence. + // If that's not actually the case, bail out. + if pos <= posOfLastNameSeen { + return false + } + posOfLastNameSeen = pos + if comma < 0 { // We've now processed all the names in csv. + break + } + csv = csv[comma+1:] + } + return true +} + +// TODO: when updating go directive to 1.21 or later, +// use min builtin instead. +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// TODO: when updating go directive to 1.21 or later, +// use max builtin instead. +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/rs/cors/utils.go b/vendor/github.com/rs/cors/utils.go index ca9983d3f71..7019f45cd9c 100644 --- a/vendor/github.com/rs/cors/utils.go +++ b/vendor/github.com/rs/cors/utils.go @@ -1,72 +1,34 @@ package cors import ( + "net/http" "strings" ) -type converter func(string) string - type wildcard struct { prefix string suffix string } func (w wildcard) match(s string) bool { - return len(s) >= len(w.prefix)+len(w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) -} - -// split compounded header values ["foo, bar", "baz"] -> ["foo", "bar", "baz"] -func splitHeaderValues(values []string) []string { - out := values - copied := false - for i, v := range values { - needsSplit := strings.IndexByte(v, ',') != -1 - if !copied { - if needsSplit { - split := strings.Split(v, ",") - out = make([]string, i, len(values)+len(split)-1) - copy(out, values[:i]) - for _, s := range split { - out = append(out, strings.TrimSpace(s)) - } - copied = true - } - } else { - if needsSplit { - split := strings.Split(v, ",") - for _, s := range split { - out = append(out, strings.TrimSpace(s)) - } - } else { - out = append(out, v) - } - } - } - return out + return len(s) >= len(w.prefix)+len(w.suffix) && + strings.HasPrefix(s, w.prefix) && + strings.HasSuffix(s, w.suffix) } // convert converts a list of string using the passed converter function -func convert(s []string, c converter) []string { - out, _ := convertDidCopy(s, c) +func convert(s []string, f func(string) string) []string { + out := make([]string, len(s)) + for i := range s { + out[i] = f(s[i]) + } return out } -// convertDidCopy is same as convert but returns true if it copied the slice -func convertDidCopy(s []string, c converter) ([]string, bool) { - out := s - copied := false - for i, v := range s { - if !copied { - v2 := c(v) - if v2 != v { - out = make([]string, len(s)) - copy(out, s[:i]) - out[i] = v2 - copied = true - } - } else { - out[i] = c(v) - } +func first(hdrs http.Header, k string) ([]string, bool) { + v, found := hdrs[k] + if !found || len(v) == 0 { + return nil, false } - return out, copied + return v[:1], true } diff --git a/vendor/modules.txt b/vendor/modules.txt index 80fa6dca003..8cff588ff75 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1673,9 +1673,10 @@ github.com/rogpeppe/go-internal/internal/syscall/windows github.com/rogpeppe/go-internal/internal/syscall/windows/sysdll github.com/rogpeppe/go-internal/lockedfile github.com/rogpeppe/go-internal/lockedfile/internal/filelock -# github.com/rs/cors v1.10.1 +# github.com/rs/cors v1.11.0 ## explicit; go 1.13 github.com/rs/cors +github.com/rs/cors/internal # github.com/rs/xid v1.5.0 ## explicit; go 1.12 github.com/rs/xid