CVE-2020-15233 (published 2020-10-02T21:15Z)

ORY Fosite is a security first OAuth2 & OpenID Connect framework for Go. In Fosite from version 0.30.2 and before version 0.34.1, there is an issue in which an an attacker can override the registered redirect URL by performing an OAuth flow and requesting a redirect URL that is to the loopback adapter. Attackers can provide both custom URL query parameters to their loopback redirect URL, as well as actually overriding the host of the registered redirect URL. These attacks are only applicable in scenarios where the attacker has access over the loopback interface. This vulnerability has been patched in ORY Fosite v0.34.1.

Severity: MEDIUM
CVSS v2 base score: 4.9 (AV:N/AC:M/Au:S/C:P/I:P/A:N)
CVSS v3 base score: 4.8, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/ory/fosite
commit_details: repo_url: https://github.com/ory/fosite
author: aeneasr
author_date: 2020-10-02 12:26:51+02:00
msg: fix: make redirect URL checking more strict

The OAuth 2.0 Client's Redirect URL and the Redirect URL used in the OAuth 2.0 flow do not check if the query string is equal:

1. Registering a client with allowed redirect URL `https://example.com/callback`
2. Performing OAuth2 flow and requesting redirect URL `https://example.com/callback?bar=foo`
3. Instead of an error, the browser is redirected to `https://example.com/callback?bar=foo` with a potentially successful OAuth2 response.

Additionally, matching Redirect URLs used `strings.ToLower` normalization:

1. Registering a client with allowed redirect URL `https://example.com/callback`
2. Performing OAuth2 flow and requesting redirect URL `https://example.com/CALLBACK`
3. Instead of an error, the browser is redirected to `https://example.com/CALLBACK ` with a potentially successful OAuth2 response.

This patch addresses all of these issues and adds regression tests to keep the implementation secure in future releases.
repository: repo_url: https://github.com/ory/fosite
repo_name: ory/fosite
description: Extensible security first OAuth 2.0 and OpenID Connect SDK for Go.
repo_language: Go
owner: ory
file_changes: filename: authorize_helper.go
change_type: ModificationType.MODIFY
diff: @@ -118,24 +118,34 @@ func isMatchingRedirectURI(uri string, haystack []string) bool {
 	}
 
 	for _, b := range haystack {
-		if strings.ToLower(b) == strings.ToLower(uri) || isLoopbackURI(requested, b) {
+		if b == uri || isMatchingAsLoopback(requested, b) {
 			return true
 		}
 	}
 	return false
 }
 
-func isLoopbackURI(requested *url.URL, registeredURI string) bool {
+func isMatchingAsLoopback(requested *url.URL, registeredURI string) bool {
 	registered, err := url.Parse(registeredURI)
 	if err != nil {
 		return false
 	}
 
-	if registered.Scheme != "http" || !isLoopbackAddress(registered.Host) {
-		return false
-	}
-
-	if requested.Scheme == "http" && isLoopbackAddress(requested.Host) && registered.Path == requested.Path {
+	// Native apps that are able to open a port on the loopback network
+	// interface without needing special permissions (typically, those on
+	// desktop operating systems) can use the loopback interface to receive
+	// the OAuth redirect.
+	//
+	// Loopback redirect URIs use the "http" scheme and are constructed with
+	// the loopback IP literal and whatever port the client is listening on.
+	//
+	// Source: https://tools.ietf.org/html/rfc8252#section-7.3
+	if requested.Scheme == "http" &&
+		isLoopbackAddress(requested.Host) &&
+		registered.Hostname() == requested.Hostname() &&
+		// The port is skipped here - see codedoc above!
+		registered.Path == requested.Path &&
+		registered.RawQuery == requested.RawQuery {
 		return true
 	}
 

programming_language: Go
filename: authorize_helper_test.go
change_type: ModificationType.MODIFY
diff: @@ -137,18 +137,6 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 			isError:  false,
 			expected: "https://bar.com/cb",
 		},
-		{
-			client:   &DefaultClient{RedirectURIs: []string{"https://bar.Com/cb"}},
-			url:      "https://bar.com/cb",
-			isError:  false,
-			expected: "https://bar.com/cb",
-		},
-		{
-			client:   &DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
-			url:      "https://bar.Com/cb",
-			isError:  false,
-			expected: "https://bar.Com/cb",
-		},
 		{
 			client:  &DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
 			url:     "https://bar.com/cb123",
@@ -157,8 +145,8 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://[::1]"}},
 			url:      "http://[::1]:1024",
-			isError:  false,
 			expected: "http://[::1]:1024",
+			isError:  false,
 		},
 		{
 			client:  &DefaultClient{RedirectURIs: []string{"http://[::1]"}},
@@ -168,8 +156,8 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://[::1]/cb"}},
 			url:      "http://[::1]:1024/cb",
-			isError:  false,
 			expected: "http://[::1]:1024/cb",
+			isError:  false,
 		},
 		{
 			client:  &DefaultClient{RedirectURIs: []string{"http://[::1]"}},
@@ -179,14 +167,14 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
 			url:      "http://127.0.0.1:1024",
-			isError:  false,
 			expected: "http://127.0.0.1:1024",
+			isError:  false,
 		},
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://127.0.0.1/cb"}},
 			url:      "http://127.0.0.1:64000/cb",
-			isError:  false,
 			expected: "http://127.0.0.1:64000/cb",
+			isError:  false,
 		},
 		{
 			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
@@ -196,14 +184,14 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
 			url:      "http://127.0.0.1",
-			isError:  false,
 			expected: "http://127.0.0.1",
+			isError:  false,
 		},
 		{
 			client:   &DefaultClient{RedirectURIs: []string{"http://127.0.0.1/Cb"}},
 			url:      "http://127.0.0.1:8080/Cb",
-			isError:  false,
 			expected: "http://127.0.0.1:8080/Cb",
+			isError:  false,
 		},
 		{
 			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
@@ -215,9 +203,45 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
 			url:     ":/invalid.uri)bar",
 			isError: true,
 		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
+			url:     "http://127.0.0.1:8080/Cb",
+			isError: true,
+		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
+			url:     "http://127.0.0.1:8080/cb?foo=bar",
+			isError: true,
+		},
+		{
+			client:   &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar"}},
+			url:      "http://127.0.0.1:8080/cb?foo=bar",
+			expected: "http://127.0.0.1:8080/cb?foo=bar",
+			isError:  false,
+		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar"}},
+			url:     "http://127.0.0.1:8080/cb?baz=bar&foo=bar",
+			isError: true,
+		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar&baz=bar"}},
+			url:     "http://127.0.0.1:8080/cb?baz=bar&foo=bar",
+			isError: true,
+		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"https://www.ory.sh/cb"}},
+			url:     "http://127.0.0.1:8080/cb",
+			isError: true,
+		},
+		{
+			client:  &DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
+			url:     "https://www.ory.sh/cb",
+			isError: true,
+		},
 	} {
 		redir, err := MatchRedirectURIWithClientRedirectURIs(c.url, c.client)
-		assert.Equal(t, c.isError, err != nil, "%d: %s", k, err)
+		assert.Equal(t, c.isError, err != nil, "%d: %+v", k, c)
 		if err == nil {
 			require.NotNil(t, redir, "%d", k)
 			assert.Equal(t, c.expected, redir.String(), "%d", k)

programming_language: Go
filename: helper_test.go
change_type: ModificationType.MODIFY
diff: @@ -51,7 +51,7 @@ func TestEscapeJSONString(t *testing.T) {
 	for _, str := range []string{"", "foobar", `foo"bar`, `foo\bar`, "foo\n\tbar"} {
 		escaped := EscapeJSONString(str)
 		var unmarshaled string
-		err := json.Unmarshal([]byte(`"` + escaped + `"`), &unmarshaled)
+		err := json.Unmarshal([]byte(`"`+escaped+`"`), &unmarshaled)
 		require.NoError(t, err, str)
 		assert.Equal(t, str, unmarshaled, str)
 	}

programming_language: Go