Skip to content

Commit 2d323f9

Browse files
dkumorbradfitz
authored andcommitted
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 in 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 Change-Id: I45886aee548431fe4031883ab1629a41e35f1727 GitHub-Last-Rev: 7be6b8d GitHub-Pull-Request: #36378 Reviewed-on: https://go-review.googlesource.com/c/go/+/213257 Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 9439a7d commit 2d323f9

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

src/net/http/httputil/reverseproxy.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ func singleJoiningSlash(a, b string) string {
110110
return a + b
111111
}
112112

113+
func joinURLPath(a, b *url.URL) (path, rawpath string) {
114+
if a.RawPath == "" && b.RawPath == "" {
115+
return singleJoiningSlash(a.Path, b.Path), ""
116+
}
117+
// Same as singleJoiningSlash, but uses EscapedPath to determine
118+
// whether a slash should be added
119+
apath := a.EscapedPath()
120+
bpath := b.EscapedPath()
121+
122+
aslash := strings.HasSuffix(apath, "/")
123+
bslash := strings.HasPrefix(bpath, "/")
124+
125+
switch {
126+
case aslash && bslash:
127+
return a.Path + b.Path[1:], apath + bpath[1:]
128+
case !aslash && !bslash:
129+
return a.Path + "/" + b.Path, apath + "/" + bpath
130+
}
131+
return a.Path + b.Path, apath + bpath
132+
}
133+
113134
// NewSingleHostReverseProxy returns a new ReverseProxy that routes
114135
// URLs to the scheme, host, and base path provided in target. If the
115136
// target's path is "/base" and the incoming request was for "/dir",
@@ -122,7 +143,7 @@ func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
122143
director := func(req *http.Request) {
123144
req.URL.Scheme = target.Scheme
124145
req.URL.Host = target.Host
125-
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
146+
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
126147
if targetQuery == "" || req.URL.RawQuery == "" {
127148
req.URL.RawQuery = targetQuery + req.URL.RawQuery
128149
} else {

src/net/http/httputil/reverseproxy_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1357,11 +1357,38 @@ func TestSingleJoinSlash(t *testing.T) {
13571357
}
13581358
for _, tt := range tests {
13591359
if got := singleJoiningSlash(tt.slasha, tt.slashb); got != tt.expected {
1360-
t.Errorf("singleJoiningSlash(%s,%s) want %s got %s",
1360+
t.Errorf("singleJoiningSlash(%q,%q) want %q got %q",
13611361
tt.slasha,
13621362
tt.slashb,
13631363
tt.expected,
13641364
got)
13651365
}
13661366
}
13671367
}
1368+
1369+
func TestJoinURLPath(t *testing.T) {
1370+
tests := []struct {
1371+
a *url.URL
1372+
b *url.URL
1373+
wantPath string
1374+
wantRaw string
1375+
}{
1376+
{&url.URL{Path: "/a/b"}, &url.URL{Path: "/c"}, "/a/b/c", ""},
1377+
{&url.URL{Path: "/a/b", RawPath: "badpath"}, &url.URL{Path: "c"}, "/a/b/c", "/a/b/c"},
1378+
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
1379+
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
1380+
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb%2F"}, &url.URL{Path: "c"}, "/a/b//c", "/a%2Fb%2F/c"},
1381+
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb/"}, &url.URL{Path: "/c/d", RawPath: "/c%2Fd"}, "/a/b/c/d", "/a%2Fb/c%2Fd"},
1382+
}
1383+
1384+
for _, tt := range tests {
1385+
p, rp := joinURLPath(tt.a, tt.b)
1386+
if p != tt.wantPath || rp != tt.wantRaw {
1387+
t.Errorf("joinURLPath(URL(%q,%q),URL(%q,%q)) want (%q,%q) got (%q,%q)",
1388+
tt.a.Path, tt.a.RawPath,
1389+
tt.b.Path, tt.b.RawPath,
1390+
tt.wantPath, tt.wantRaw,
1391+
p, rp)
1392+
}
1393+
}
1394+
}

0 commit comments

Comments
 (0)