Skip to content

Commit f6d8445

Browse files
neilddmitshur
authored andcommitted
[release-branch.go1.19] net/http/httputil: avoid query parameter smuggling
Query parameter smuggling occurs when a proxy's interpretation of query parameters differs from that of a downstream server. Change ReverseProxy to avoid forwarding ignored query parameters. Remove unparsable query parameters from the outbound request * if req.Form != nil after calling ReverseProxy.Director; and * before calling ReverseProxy.Rewrite. This change preserves the existing behavior of forwarding the raw query untouched if a Director hook does not parse the query by calling Request.ParseForm (possibly indirectly). Fixes #55843 For #54663 For CVE-2022-2880 Change-Id: If1621f6b0e73a49d79059dae9e6b256e0ff18ca9 Reviewed-on: https://go-review.googlesource.com/c/go/+/432976 Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Damien Neil <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/433735 Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 2614985 commit f6d8445

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

src/net/http/httputil/reverseproxy.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
261261
}
262262

263263
p.Director(outreq)
264+
if outreq.Form != nil {
265+
outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
266+
}
264267
outreq.Close = false
265268

266269
reqUpType := upgradeType(outreq.Header)
@@ -639,3 +642,36 @@ func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
639642
_, err := io.Copy(c.backend, c.user)
640643
errc <- err
641644
}
645+
646+
func cleanQueryParams(s string) string {
647+
reencode := func(s string) string {
648+
v, _ := url.ParseQuery(s)
649+
return v.Encode()
650+
}
651+
for i := 0; i < len(s); {
652+
switch s[i] {
653+
case ';':
654+
return reencode(s)
655+
case '%':
656+
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
657+
return reencode(s)
658+
}
659+
i += 3
660+
default:
661+
i++
662+
}
663+
}
664+
return s
665+
}
666+
667+
func ishex(c byte) bool {
668+
switch {
669+
case '0' <= c && c <= '9':
670+
return true
671+
case 'a' <= c && c <= 'f':
672+
return true
673+
case 'A' <= c && c <= 'F':
674+
return true
675+
}
676+
return false
677+
}

src/net/http/httputil/reverseproxy_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,3 +1537,77 @@ func TestJoinURLPath(t *testing.T) {
15371537
}
15381538
}
15391539
}
1540+
1541+
const (
1542+
testWantsCleanQuery = true
1543+
testWantsRawQuery = false
1544+
)
1545+
1546+
func TestReverseProxyQueryParameterSmugglingDirectorDoesNotParseForm(t *testing.T) {
1547+
testReverseProxyQueryParameterSmuggling(t, testWantsRawQuery, func(u *url.URL) *ReverseProxy {
1548+
proxyHandler := NewSingleHostReverseProxy(u)
1549+
oldDirector := proxyHandler.Director
1550+
proxyHandler.Director = func(r *http.Request) {
1551+
oldDirector(r)
1552+
}
1553+
return proxyHandler
1554+
})
1555+
}
1556+
1557+
func TestReverseProxyQueryParameterSmugglingDirectorParsesForm(t *testing.T) {
1558+
testReverseProxyQueryParameterSmuggling(t, testWantsCleanQuery, func(u *url.URL) *ReverseProxy {
1559+
proxyHandler := NewSingleHostReverseProxy(u)
1560+
oldDirector := proxyHandler.Director
1561+
proxyHandler.Director = func(r *http.Request) {
1562+
// Parsing the form causes ReverseProxy to remove unparsable
1563+
// query parameters before forwarding.
1564+
r.FormValue("a")
1565+
oldDirector(r)
1566+
}
1567+
return proxyHandler
1568+
})
1569+
}
1570+
1571+
func testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuery bool, newProxy func(*url.URL) *ReverseProxy) {
1572+
const content = "response_content"
1573+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1574+
w.Write([]byte(r.URL.RawQuery))
1575+
}))
1576+
defer backend.Close()
1577+
backendURL, err := url.Parse(backend.URL)
1578+
if err != nil {
1579+
t.Fatal(err)
1580+
}
1581+
proxyHandler := newProxy(backendURL)
1582+
frontend := httptest.NewServer(proxyHandler)
1583+
defer frontend.Close()
1584+
1585+
// Don't spam output with logs of queries containing semicolons.
1586+
backend.Config.ErrorLog = log.New(io.Discard, "", 0)
1587+
frontend.Config.ErrorLog = log.New(io.Discard, "", 0)
1588+
1589+
for _, test := range []struct {
1590+
rawQuery string
1591+
cleanQuery string
1592+
}{{
1593+
rawQuery: "a=1&a=2;b=3",
1594+
cleanQuery: "a=1",
1595+
}, {
1596+
rawQuery: "a=1&a=%zz&b=3",
1597+
cleanQuery: "a=1&b=3",
1598+
}} {
1599+
res, err := frontend.Client().Get(frontend.URL + "?" + test.rawQuery)
1600+
if err != nil {
1601+
t.Fatalf("Get: %v", err)
1602+
}
1603+
defer res.Body.Close()
1604+
body, _ := io.ReadAll(res.Body)
1605+
wantQuery := test.rawQuery
1606+
if wantCleanQuery {
1607+
wantQuery = test.cleanQuery
1608+
}
1609+
if got, want := string(body), wantQuery; got != want {
1610+
t.Errorf("proxy forwarded raw query %q as %q, want %q", test.rawQuery, got, want)
1611+
}
1612+
}
1613+
}

0 commit comments

Comments
 (0)