CVE-2022-1996: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Authorization Bypass Through User-Controlled Key in GitHub repository emicklei/go-restful prior to v3.8.0.
CWE description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
CVSS v3 base score: 9.1
Published: 2022-06-08T13:15Z
Repository: https://github.com/emicklei/go-restful
Language: Go
Files changed: cors_filter.go, cors_filter_test.go
Commit date: 2022-06-06 07:45:08 +0200
Commit message: use exact matching of allowed domain entries, issue #489 (#493)

* use exact matching of allowed domain entries, issue #489

* update doc, add testcases from PR conversation

* introduce AllowedDomainFunc #489

* more tests, fix doc

* lowercase origin before checking cors
Diff: diff --git a/cors_filter.go b/cors_filter.go
index d6e7c85..9d18dfb 100644
--- a/cors_filter.go
+++ b/cors_filter.go
@@ -16,13 +16,26 @@ import (
 //
 // http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
 // http://enable-cors.org/server.html
 // http://www.html5rocks.com/en/tutorials/cors/#toc-handling-a-not-so-simple-request
 type CrossOriginResourceSharing struct {
-	ExposeHeaders  []string // list of Header names
-	AllowedHeaders []string // list of Header names
-	AllowedDomains []string // list of allowed values for Http Origin. An allowed value can be a regular expression to support subdomain matching. If empty all are allowed.
+	ExposeHeaders []string // list of Header names
+
+	// AllowedHeaders is alist of Header names. Checking is case-insensitive.
+	// The list may contain the special wildcard string ".*" ; all is allowed
+	AllowedHeaders []string
+
+	// AllowedDomains is a list of allowed values for Http Origin.
+	// The list may contain the special wildcard string ".*" ; all is allowed
+	// If empty all are allowed.
+	AllowedDomains []string
+
+	// AllowedDomainFunc is optional and is a function that will do the check
+	// when the origin is not part of the AllowedDomains and it does not contain the wildcard ".*".
+	AllowedDomainFunc func(origin string) bool
+
+	// AllowedMethods is either empty or has a list of http methods names. Checking is case-insensitive.
 	AllowedMethods []string
 	MaxAge         int // number of seconds before requiring new Options request
 	CookiesAllowed bool
 	Container      *Container
 
@@ -117,40 +130,28 @@ func (c CrossOriginResourceSharing) setOptionsHeaders(req *Request, resp *Respon
 
 func (c CrossOriginResourceSharing) isOriginAllowed(origin string) bool {
 	if len(origin) == 0 {
 		return false
 	}
+	lowerOrigin := strings.ToLower(origin)
 	if len(c.AllowedDomains) == 0 {
+		if c.AllowedDomainFunc != nil {
+			return c.AllowedDomainFunc(lowerOrigin)
+		}
 		return true
 	}
 
-	allowed := false
+	// exact match on each allowed domain
 	for _, domain := range c.AllowedDomains {
-		if domain == origin {
-			allowed = true
-			break
+		if domain == ".*" || strings.ToLower(domain) == lowerOrigin {
+			return true
 		}
 	}
-
-	if !allowed {
-		if len(c.allowedOriginPatterns) == 0 {
-			// compile allowed domains to allowed origin patterns
-			allowedOriginRegexps, err := compileRegexps(c.AllowedDomains)
-			if err != nil {
-				return false
-			}
-			c.allowedOriginPatterns = allowedOriginRegexps
-		}
-
-		for _, pattern := range c.allowedOriginPatterns {
-			if allowed = pattern.MatchString(origin); allowed {
-				break
-			}
-		}
+	if c.AllowedDomainFunc != nil {
+		return c.AllowedDomainFunc(origin)
 	}
-
-	return allowed
+	return false
 }
 
 func (c CrossOriginResourceSharing) setAllowOriginHeader(req *Request, resp *Response) {
 	origin := req.Request.Header.Get(HEADER_Origin)
 	if c.isOriginAllowed(origin) {
@@ -188,18 +189,5 @@ func (c CrossOriginResourceSharing) isValidAccessControlRequestHeader(header str
 			return true
 		}
 	}
 	return false
 }
-
-// Take a list of strings and compile them into a list of regular expressions.
-func compileRegexps(regexpStrings []string) ([]*regexp.Regexp, error) {
-	regexps := []*regexp.Regexp{}
-	for _, regexpStr := range regexpStrings {
-		r, err := regexp.Compile(regexpStr)
-		if err != nil {
-			return regexps, err
-		}
-		regexps = append(regexps, r)
-	}
-	return regexps, nil
-}
diff --git a/cors_filter_test.go b/cors_filter_test.go
index 09c5d33..5a620c0 100644
--- a/cors_filter_test.go
+++ b/cors_filter_test.go
@@ -118,12 +118,48 @@ func TestCORSFilter_AllowedDomains(t *testing.T) {
 		httpRequest.Header.Set(HEADER_Origin, each.origin)
 		httpWriter := httptest.NewRecorder()
 		DefaultContainer.Dispatch(httpWriter, httpRequest)
 		actual := httpWriter.Header().Get(HEADER_AccessControlAllowOrigin)
 		if actual != each.origin && each.allowed {
-			t.Fatal("expected to be accepted")
+			t.Error("expected to be accepted", each)
 		}
 		if actual == each.origin && !each.allowed {
-			t.Fatal("did not expect to be accepted")
+			t.Error("did not expect to be accepted")
 		}
 	}
 }
+
+func TestCORSFilter_AllowedDomainFunc(t *testing.T) {
+	cors := CrossOriginResourceSharing{
+		AllowedDomains: []string{"here", "there"},
+		AllowedDomainFunc: func(origin string) bool {
+			return "where" == origin
+		},
+	}
+	if got, want := cors.isOriginAllowed("here"), true; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	if got, want := cors.isOriginAllowed("HERE"), true; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	if got, want := cors.isOriginAllowed("there"), true; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	if got, want := cors.isOriginAllowed("where"), true; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	if got, want := cors.isOriginAllowed("nowhere"), false; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	// just func
+	cors.AllowedDomains = []string{}
+	if got, want := cors.isOriginAllowed("here"), false; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	if got, want := cors.isOriginAllowed("where"), true; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+	// empty domain
+	if got, want := cors.isOriginAllowed(""), false; got != want {
+		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
+	}
+}

Vulnerable code: 	ExposeHeaders  []string // list of Header names
	AllowedHeaders []string // list of Header names
	AllowedDomains []string // list of allowed values for Http Origin. An allowed value can be a regular expression to support subdomain matching. If empty all are allowed.
	allowed := false
		if domain == origin {
			allowed = true
			break

	if !allowed {
		if len(c.allowedOriginPatterns) == 0 {
			// compile allowed domains to allowed origin patterns
			allowedOriginRegexps, err := compileRegexps(c.AllowedDomains)
			if err != nil {
				return false
			}
			c.allowedOriginPatterns = allowedOriginRegexps
		}

		for _, pattern := range c.allowedOriginPatterns {
			if allowed = pattern.MatchString(origin); allowed {
				break
			}
		}

	return allowed

// Take a list of strings and compile them into a list of regular expressions.
func compileRegexps(regexpStrings []string) ([]*regexp.Regexp, error) {
	regexps := []*regexp.Regexp{}
	for _, regexpStr := range regexpStrings {
		r, err := regexp.Compile(regexpStr)
		if err != nil {
			return regexps, err
		}
		regexps = append(regexps, r)
	}
	return regexps, nil
}
			t.Fatal("expected to be accepted")
			t.Fatal("did not expect to be accepted")
Fixed code: 	ExposeHeaders []string // list of Header names

	// AllowedHeaders is alist of Header names. Checking is case-insensitive.
	// The list may contain the special wildcard string ".*" ; all is allowed
	AllowedHeaders []string

	// AllowedDomains is a list of allowed values for Http Origin.
	// The list may contain the special wildcard string ".*" ; all is allowed
	// If empty all are allowed.
	AllowedDomains []string

	// AllowedDomainFunc is optional and is a function that will do the check
	// when the origin is not part of the AllowedDomains and it does not contain the wildcard ".*".
	AllowedDomainFunc func(origin string) bool

	// AllowedMethods is either empty or has a list of http methods names. Checking is case-insensitive.
	lowerOrigin := strings.ToLower(origin)
		if c.AllowedDomainFunc != nil {
			return c.AllowedDomainFunc(lowerOrigin)
		}
	// exact match on each allowed domain
		if domain == ".*" || strings.ToLower(domain) == lowerOrigin {
			return true
	if c.AllowedDomainFunc != nil {
		return c.AllowedDomainFunc(origin)
	return false
			t.Error("expected to be accepted", each)
			t.Error("did not expect to be accepted")

func TestCORSFilter_AllowedDomainFunc(t *testing.T) {
	cors := CrossOriginResourceSharing{
		AllowedDomains: []string{"here", "there"},
		AllowedDomainFunc: func(origin string) bool {
			return "where" == origin
		},
	}
	if got, want := cors.isOriginAllowed("here"), true; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	if got, want := cors.isOriginAllowed("HERE"), true; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	if got, want := cors.isOriginAllowed("there"), true; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	if got, want := cors.isOriginAllowed("where"), true; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	if got, want := cors.isOriginAllowed("nowhere"), false; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	// just func
	cors.AllowedDomains = []string{}
	if got, want := cors.isOriginAllowed("here"), false; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	if got, want := cors.isOriginAllowed("where"), true; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
	// empty domain
	if got, want := cors.isOriginAllowed(""), false; got != want {
		t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want)
	}
}