diff --git a/proxy/proxy.go b/proxy/proxy.go index e21f253adb..5fafae70bf 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -184,20 +184,46 @@ func ConfigureBackendURL(r *http.Request, rl *rule.Rule) error { } proxyHost := r.Host - proxyPath := r.URL.Path + proxyPath := r.URL.EscapedPath() backendHost := p.Host - backendPath := p.Path + backendPath := p.EscapedPath() backendScheme := p.Scheme + // We need to build a new path from the incoming *escaped* path components, + // because just relying on the already-escaped URL.Path results in escaped + // slashes (%2F) being irreversibly converted to actual slashes. + // + // However, once the new escaped path is built, we can't immediately assign it + // to the resulting URL: + // - Assigning to URL.Path would mean assigning an escaped path to + // the unescaped path attribute, which would end up double-escaping + // everything. + // - Setting RawPath on its own will result in its value being ignored, + // because URL.EscapedPath() (used by URL.String()) will ignore RawPath + // if it does not match an escaped version of Path. + // - Directly escaping the path ourselves first to assign it to Path isn't + // possible, because net/url exposes *no* way to escape an entire path, + // only path segments. + // + // In order to be able to set both URL.Path and URL.RawPath, we re-parse the + // escaped path as an intermediate URL. This gives us the correct Path and + // RawPath values that can then be set on the forward URL. + newEscapedPath := "/" + strings.TrimLeft("/"+strings.Trim(backendPath, "/")+"/"+strings.TrimLeft(proxyPath, "/"), "/") + if rl.Upstream.StripPath != "" { + newEscapedPath = strings.Replace(newEscapedPath, "/"+strings.Trim(rl.Upstream.StripPath, "/"), "", 1) + } + + intermediatePathURL, err := url.Parse(newEscapedPath) + if err != nil { + return errors.WithStack(err) + } + forwardURL := r.URL forwardURL.Scheme = backendScheme forwardURL.Host = backendHost - forwardURL.Path = "/" + strings.TrimLeft("/"+strings.Trim(backendPath, "/")+"/"+strings.TrimLeft(proxyPath, "/"), "/") - - if rl.Upstream.StripPath != "" { - forwardURL.Path = strings.Replace(forwardURL.Path, "/"+strings.Trim(rl.Upstream.StripPath, "/"), "", 1) - } + forwardURL.RawPath = intermediatePathURL.RawPath + forwardURL.Path = intermediatePathURL.Path r.Host = backendHost if rl.Upstream.PreserveHost { diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 579919529a..665f9c0f6f 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -501,6 +501,12 @@ func TestConfigureBackendURL(t *testing.T) { eURL: "http://localhost:4000/foo/users/1234", eHost: "localhost:3000", }, + { + r: &http.Request{Host: "localhost:3000", URL: &url.URL{RawPath: "/api/users/12%2F34", Path: "/api/users/12/34", Scheme: "http"}}, + rl: &rule.Rule{Upstream: rule.Upstream{URL: "http://localhost:4000/foo/", PreserveHost: true, StripPath: "api"}}, + eURL: "http://localhost:4000/foo/users/12%2F34", + eHost: "localhost:3000", + }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { require.NoError(t, proxy.ConfigureBackendURL(tc.r, tc.rl))