Skip to content

Commit cde1dda

Browse files
neildgopherbot
authored andcommitted
proxy, http/httpproxy: do not mismatch IPv6 zone ids against hosts
When matching against a host "example.com", don't match an IPv6 address like "[1000::1%25.example.com]:80". Thanks to Juho Forsén of Mattermost for reporting this issue. Fixes CVE-2025-22870 For #71984 Change-Id: I0c4fdf18765decc27e6ddf220ebe3a9bf4a6454d Reviewed-on: https://go-review.googlesource.com/c/net/+/654697 Auto-Submit: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Commit-Queue: Roland Shoemaker <roland@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org> Reviewed-by: Damien Neil <dneil@google.com>
1 parent fe7f039 commit cde1dda

File tree

4 files changed

+131
-52
lines changed

4 files changed

+131
-52
lines changed

http/httpproxy/proxy.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"errors"
1515
"fmt"
1616
"net"
17+
"net/netip"
1718
"net/url"
1819
"os"
1920
"strings"
@@ -177,8 +178,10 @@ func (cfg *config) useProxy(addr string) bool {
177178
if host == "localhost" {
178179
return false
179180
}
180-
ip := net.ParseIP(host)
181-
if ip != nil {
181+
nip, err := netip.ParseAddr(host)
182+
var ip net.IP
183+
if err == nil {
184+
ip = net.IP(nip.AsSlice())
182185
if ip.IsLoopback() {
183186
return false
184187
}
@@ -360,6 +363,9 @@ type domainMatch struct {
360363
}
361364

362365
func (m domainMatch) match(host, port string, ip net.IP) bool {
366+
if ip != nil {
367+
return false
368+
}
363369
if strings.HasSuffix(host, m.host) || (m.matchHost && host == m.host[1:]) {
364370
return m.port == "" || m.port == port
365371
}

http/httpproxy/proxy_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ var proxyForURLTests = []proxyForURLTest{{
211211
},
212212
req: "http://www.xn--fsq092h.com",
213213
want: "<nil>",
214+
}, {
215+
cfg: httpproxy.Config{
216+
NoProxy: "example.com",
217+
HTTPProxy: "proxy",
218+
},
219+
req: "http://[1000::%25.example.com]:123",
220+
want: "http://proxy",
214221
},
215222
}
216223

proxy/per_host.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package proxy
77
import (
88
"context"
99
"net"
10+
"net/netip"
1011
"strings"
1112
)
1213

@@ -57,7 +58,8 @@ func (p *PerHost) DialContext(ctx context.Context, network, addr string) (c net.
5758
}
5859

5960
func (p *PerHost) dialerForRequest(host string) Dialer {
60-
if ip := net.ParseIP(host); ip != nil {
61+
if nip, err := netip.ParseAddr(host); err == nil {
62+
ip := net.IP(nip.AsSlice())
6163
for _, net := range p.bypassNetworks {
6264
if net.Contains(ip) {
6365
return p.bypass
@@ -108,8 +110,8 @@ func (p *PerHost) AddFromString(s string) {
108110
}
109111
continue
110112
}
111-
if ip := net.ParseIP(host); ip != nil {
112-
p.AddIP(ip)
113+
if nip, err := netip.ParseAddr(host); err == nil {
114+
p.AddIP(net.IP(nip.AsSlice()))
113115
continue
114116
}
115117
if strings.HasPrefix(host, "*.") {

proxy/per_host_test.go

+111-47
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ package proxy
77
import (
88
"context"
99
"errors"
10+
"fmt"
1011
"net"
11-
"reflect"
12+
"slices"
1213
"testing"
1314
)
1415

@@ -22,55 +23,118 @@ func (r *recordingProxy) Dial(network, addr string) (net.Conn, error) {
2223
}
2324

2425
func TestPerHost(t *testing.T) {
25-
expectedDef := []string{
26-
"example.com:123",
27-
"1.2.3.4:123",
28-
"[1001::]:123",
29-
}
30-
expectedBypass := []string{
31-
"localhost:123",
32-
"zone:123",
33-
"foo.zone:123",
34-
"127.0.0.1:123",
35-
"10.1.2.3:123",
36-
"[1000::]:123",
37-
}
38-
39-
t.Run("Dial", func(t *testing.T) {
40-
var def, bypass recordingProxy
41-
perHost := NewPerHost(&def, &bypass)
42-
perHost.AddFromString("localhost,*.zone,127.0.0.1,10.0.0.1/8,1000::/16")
43-
for _, addr := range expectedDef {
44-
perHost.Dial("tcp", addr)
26+
for _, test := range []struct {
27+
config string // passed to PerHost.AddFromString
28+
nomatch []string // addrs using the default dialer
29+
match []string // addrs using the bypass dialer
30+
}{{
31+
config: "localhost,*.zone,127.0.0.1,10.0.0.1/8,1000::/16",
32+
nomatch: []string{
33+
"example.com:123",
34+
"1.2.3.4:123",
35+
"[1001::]:123",
36+
},
37+
match: []string{
38+
"localhost:123",
39+
"zone:123",
40+
"foo.zone:123",
41+
"127.0.0.1:123",
42+
"10.1.2.3:123",
43+
"[1000::]:123",
44+
"[1000::%25.example.com]:123",
45+
},
46+
}, {
47+
config: "localhost",
48+
nomatch: []string{
49+
"127.0.0.1:80",
50+
},
51+
match: []string{
52+
"localhost:80",
53+
},
54+
}, {
55+
config: "*.zone",
56+
nomatch: []string{
57+
"foo.com:80",
58+
},
59+
match: []string{
60+
"foo.zone:80",
61+
"foo.bar.zone:80",
62+
},
63+
}, {
64+
config: "1.2.3.4",
65+
nomatch: []string{
66+
"127.0.0.1:80",
67+
"11.2.3.4:80",
68+
},
69+
match: []string{
70+
"1.2.3.4:80",
71+
},
72+
}, {
73+
config: "10.0.0.0/24",
74+
nomatch: []string{
75+
"10.0.1.1:80",
76+
},
77+
match: []string{
78+
"10.0.0.1:80",
79+
"10.0.0.255:80",
80+
},
81+
}, {
82+
config: "fe80::/10",
83+
nomatch: []string{
84+
"[fec0::1]:80",
85+
"[fec0::1%en0]:80",
86+
},
87+
match: []string{
88+
"[fe80::1]:80",
89+
"[fe80::1%en0]:80",
90+
},
91+
}, {
92+
// We don't allow zone IDs in network prefixes,
93+
// so this config matches nothing.
94+
config: "fe80::%en0/10",
95+
nomatch: []string{
96+
"[fec0::1]:80",
97+
"[fec0::1%en0]:80",
98+
"[fe80::1]:80",
99+
"[fe80::1%en0]:80",
100+
"[fe80::1%en1]:80",
101+
},
102+
}} {
103+
for _, addr := range test.match {
104+
testPerHost(t, test.config, addr, true)
45105
}
46-
for _, addr := range expectedBypass {
47-
perHost.Dial("tcp", addr)
106+
for _, addr := range test.nomatch {
107+
testPerHost(t, test.config, addr, false)
48108
}
109+
}
110+
}
49111

50-
if !reflect.DeepEqual(expectedDef, def.addrs) {
51-
t.Errorf("Hosts which went to the default proxy didn't match. Got %v, want %v", def.addrs, expectedDef)
52-
}
53-
if !reflect.DeepEqual(expectedBypass, bypass.addrs) {
54-
t.Errorf("Hosts which went to the bypass proxy didn't match. Got %v, want %v", bypass.addrs, expectedBypass)
55-
}
56-
})
112+
func testPerHost(t *testing.T, config, addr string, wantMatch bool) {
113+
name := fmt.Sprintf("config %q, dial %q", config, addr)
57114

58-
t.Run("DialContext", func(t *testing.T) {
59-
var def, bypass recordingProxy
60-
perHost := NewPerHost(&def, &bypass)
61-
perHost.AddFromString("localhost,*.zone,127.0.0.1,10.0.0.1/8,1000::/16")
62-
for _, addr := range expectedDef {
63-
perHost.DialContext(context.Background(), "tcp", addr)
64-
}
65-
for _, addr := range expectedBypass {
66-
perHost.DialContext(context.Background(), "tcp", addr)
67-
}
115+
var def, bypass recordingProxy
116+
perHost := NewPerHost(&def, &bypass)
117+
perHost.AddFromString(config)
118+
perHost.Dial("tcp", addr)
68119

69-
if !reflect.DeepEqual(expectedDef, def.addrs) {
70-
t.Errorf("Hosts which went to the default proxy didn't match. Got %v, want %v", def.addrs, expectedDef)
71-
}
72-
if !reflect.DeepEqual(expectedBypass, bypass.addrs) {
73-
t.Errorf("Hosts which went to the bypass proxy didn't match. Got %v, want %v", bypass.addrs, expectedBypass)
74-
}
75-
})
120+
// Dial and DialContext should have the same results.
121+
var defc, bypassc recordingProxy
122+
perHostc := NewPerHost(&defc, &bypassc)
123+
perHostc.AddFromString(config)
124+
perHostc.DialContext(context.Background(), "tcp", addr)
125+
if !slices.Equal(def.addrs, defc.addrs) {
126+
t.Errorf("%v: Dial default=%v, bypass=%v; DialContext default=%v, bypass=%v", name, def.addrs, bypass.addrs, defc.addrs, bypass.addrs)
127+
return
128+
}
129+
130+
if got, want := slices.Concat(def.addrs, bypass.addrs), []string{addr}; !slices.Equal(got, want) {
131+
t.Errorf("%v: dialed %q, want %q", name, got, want)
132+
return
133+
}
134+
135+
gotMatch := len(bypass.addrs) > 0
136+
if gotMatch != wantMatch {
137+
t.Errorf("%v: matched=%v, want %v", name, gotMatch, wantMatch)
138+
return
139+
}
76140
}

0 commit comments

Comments
 (0)