Skip to content

Commit f67619d

Browse files
committed
net/http/httputil: handle escaped paths in SingleHostReverseProxy
When forwarding a request, a SingleHostReverseProxy appends the request's path to the target URL's path. However, if certain path elements are encoded, (such as %2F for slash in either the request or target path), simply joining the URL.Path elements is not sufficient, since the field holds the *decoded* path. Since 87a605, the RawPath field was added which holds a decoding hint for the URL. When joining URL paths, this decoding hint needs to be taken into consideration. As an example, if the target URL.Path is /a/b, and URL.RawPath is /a%2Fb, joining the path with /c should result in /a/b/c URL.Path, and /a%2Fb/c in RawPath. The added joinURLPath function combines the two URL's Paths, while taking into account escaping, and replaces the previously used singleJoiningSlash in NewSingleHostReverseProxy. Fixes #35908
1 parent 0d09b7e commit f67619d

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

src/net/http/httputil/reverseproxy.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ func singleJoiningSlash(a, b string) string {
9797
return a + b
9898
}
9999

100+
func joinURLPath(a, b *url.URL) (path, rawpath string) {
101+
if a.RawPath == "" && b.RawPath == "" {
102+
return singleJoiningSlash(a.Path, b.Path), ""
103+
}
104+
// Same as singleJoiningSlash, but uses EscapedPath to determine
105+
// whether a slash should be added
106+
apath := a.EscapedPath()
107+
bpath := b.EscapedPath()
108+
109+
aslash := strings.HasSuffix(apath, "/")
110+
bslash := strings.HasPrefix(bpath, "/")
111+
112+
switch {
113+
case aslash && bslash:
114+
return a.Path + b.Path[1:], apath + bpath[1:]
115+
case !aslash && !bslash:
116+
return a.Path + "/" + b.Path, apath + "/" + bpath
117+
}
118+
return a.Path + b.Path, apath + bpath
119+
}
120+
100121
// NewSingleHostReverseProxy returns a new ReverseProxy that routes
101122
// URLs to the scheme, host, and base path provided in target. If the
102123
// target's path is "/base" and the incoming request was for "/dir",
@@ -109,7 +130,7 @@ func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
109130
director := func(req *http.Request) {
110131
req.URL.Scheme = target.Scheme
111132
req.URL.Host = target.Host
112-
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
133+
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
113134
if targetQuery == "" || req.URL.RawQuery == "" {
114135
req.URL.RawQuery = targetQuery + req.URL.RawQuery
115136
} else {

src/net/http/httputil/reverseproxy_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -1210,3 +1210,30 @@ func TestSingleJoinSlash(t *testing.T) {
12101210
}
12111211
}
12121212
}
1213+
1214+
func TestJoinURLPath(t *testing.T) {
1215+
tests := []struct {
1216+
a *url.URL
1217+
b *url.URL
1218+
path string
1219+
rawpath string
1220+
}{
1221+
{&url.URL{Path: "/a/b"}, &url.URL{Path: "/c"}, "/a/b/c", ""},
1222+
{&url.URL{Path: "/a/b", RawPath: "badpath"}, &url.URL{Path: "c"}, "/a/b/c", "/a/b/c"},
1223+
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
1224+
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
1225+
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb%2F"}, &url.URL{Path: "c"}, "/a/b//c", "/a%2Fb%2F/c"},
1226+
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb/"}, &url.URL{Path: "/c/d", RawPath: "/c%2Fd"}, "/a/b/c/d", "/a%2Fb/c%2Fd"},
1227+
}
1228+
1229+
for _, tt := range tests {
1230+
p, rp := joinURLPath(tt.a, tt.b)
1231+
if p != tt.path || rp != tt.rawpath {
1232+
t.Errorf("joinURLPath(URL(%s,%s),URL(%s,%s)) want (%s,%s) got (%s,%s)",
1233+
tt.a.Path, tt.a.RawPath,
1234+
tt.b.Path, tt.b.RawPath,
1235+
tt.path, tt.rawpath,
1236+
p, rp)
1237+
}
1238+
}
1239+
}

0 commit comments

Comments
 (0)