CVE-2020-15129: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: In Traefik before versions 1.7.26, 2.2.8, and 2.3.0-rc3, there exists a potential open redirect vulnerability in Traefik's handling of the "X-Forwarded-Prefix" header. The Traefik API dashboard component doesn't validate that the value of the header "X-Forwarded-Prefix" is a site relative path and will redirect to any header provided URI. Successful exploitation of an open redirect can be used to entice victims to disclose sensitive information. Active Exploitation of this issue is unlikely as it would require active header injection, however the Traefik team addressed this issue nonetheless to prevent abuse in e.g. cache poisoning scenarios.
CWE 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.
CVSS v3 base score: 4.7
Published: 2020-07-30T16:15Z
Repository: https://github.com/containous/traefik
Language: Go
Files changed: pkg/api/dashboard.go, pkg/api/dashboard_test.go
Commit date: 2020-07-28 10:08:03 +0200
Commit message: fix: clean X-Forwarded-Prefix header for the dashboard.



Diff: diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index e71252e4..b3168de8 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -1,9 +1,10 @@
 package api
 
 import (
 	"net/http"
+	"net/url"
 
 	"github.com/containous/traefik/v2/pkg/log"
 	assetfs "github.com/elazarl/go-bindata-assetfs"
 	"github.com/gorilla/mux"
 )
@@ -21,13 +22,31 @@ func (g DashboardHandler) Append(router *mux.Router) {
 	}
 
 	// Expose dashboard
 	router.Methods(http.MethodGet).
 		Path("/").
-		HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
-			http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound)
+		HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+			http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound)
 		})
 
 	router.Methods(http.MethodGet).
 		PathPrefix("/dashboard/").
 		Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets)))
 }
+
+func safePrefix(req *http.Request) string {
+	prefix := req.Header.Get("X-Forwarded-Prefix")
+	if prefix == "" {
+		return ""
+	}
+
+	parse, err := url.Parse(prefix)
+	if err != nil {
+		return ""
+	}
+
+	if parse.Host != "" {
+		return ""
+	}
+
+	return parse.Path
+}
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
new file mode 100644
index 00000000..c945a700
--- /dev/null
+++ b/pkg/api/dashboard_test.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_safePrefix(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		value    string
+		expected string
+	}{
+		{
+			desc:     "host",
+			value:    "https://example.com",
+			expected: "",
+		},
+		{
+			desc:     "host with path",
+			value:    "https://example.com/foo/bar?test",
+			expected: "",
+		},
+		{
+			desc:     "path",
+			value:    "/foo/bar",
+			expected: "/foo/bar",
+		},
+		{
+			desc:     "path without leading slash",
+			value:    "foo/bar",
+			expected: "foo/bar",
+		},
+	}
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			req, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
+			require.NoError(t, err)
+
+			req.Header.Set("X-Forwarded-Prefix", test.value)
+
+			prefix := safePrefix(req)
+
+			assert.Equal(t, test.expected, prefix)
+		})
+	}
+}

Vulnerable code: 		HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
			http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound)
Fixed code: 	"net/url"
		HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
			http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound)

func safePrefix(req *http.Request) string {
	prefix := req.Header.Get("X-Forwarded-Prefix")
	if prefix == "" {
		return ""
	}

	parse, err := url.Parse(prefix)
	if err != nil {
		return ""
	}

	if parse.Host != "" {
		return ""
	}

	return parse.Path
}
package api

import (
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func Test_safePrefix(t *testing.T) {
	testCases := []struct {
		desc     string
		value    string
		expected string
	}{
		{
			desc:     "host",
			value:    "https://example.com",
			expected: "",
		},
		{
			desc:     "host with path",
			value:    "https://example.com/foo/bar?test",
			expected: "",
		},
		{
			desc:     "path",
			value:    "/foo/bar",
			expected: "/foo/bar",
		},
		{
			desc:     "path without leading slash",
			value:    "foo/bar",
			expected: "foo/bar",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			req, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
			require.NoError(t, err)

			req.Header.Set("X-Forwarded-Prefix", test.value)

			prefix := safePrefix(req)

			assert.Equal(t, test.expected, prefix)
		})
	}
}