Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f5b7cd2

Browse files
committedAug 23, 2024··
fix: allow calling internal IP ranges with relevant option
The `ResilientClient` options `ResilientClientDisallowInternalIPs` and `ResilientClientAllowInternalIPRequestsTo` were not allowing to call certain IP ranges, like 100.64.0.0/10 properly.
1 parent 56eebb2 commit f5b7cd2

File tree

2 files changed

+113
-56
lines changed

2 files changed

+113
-56
lines changed
 

‎httpx/resilient_client_test.go

+109-41
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,127 @@ package httpx
55

66
import (
77
"context"
8-
"net"
8+
"fmt"
99
"net/http"
10-
"net/http/httptest"
1110
"net/http/httptrace"
1211
"net/netip"
13-
"net/url"
1412
"sync/atomic"
1513
"testing"
14+
"time"
15+
16+
"code.dny.dev/ssrf"
1617

1718
"github.com/hashicorp/go-retryablehttp"
1819
"github.com/stretchr/testify/assert"
1920
"github.com/stretchr/testify/require"
2021
)
2122

22-
func TestNoPrivateIPs(t *testing.T) {
23-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
24-
_, _ = w.Write([]byte("Hello, world!"))
25-
}))
26-
t.Cleanup(ts.Close)
27-
28-
target, err := url.ParseRequestURI(ts.URL)
29-
require.NoError(t, err)
30-
31-
_, port, err := net.SplitHostPort(target.Host)
32-
require.NoError(t, err)
33-
34-
allowedURL := "http://localhost:" + port + "/foobar"
35-
allowedGlob := "http://localhost:" + port + "/glob/*"
36-
37-
c := NewResilientClient(
38-
ResilientClientWithMaxRetry(1),
39-
ResilientClientDisallowInternalIPs(),
40-
ResilientClientAllowInternalIPRequestsTo(allowedURL, allowedGlob),
41-
)
23+
func TestPrivateIPs(t *testing.T) {
24+
testCases := []struct {
25+
url string
26+
disallowInternalIPs bool
27+
allowedIP bool
28+
}{
29+
{
30+
url: "http://127.0.0.1/foobar",
31+
disallowInternalIPs: true,
32+
allowedIP: false,
33+
},
34+
{
35+
url: "http://localhost/foobar",
36+
disallowInternalIPs: true,
37+
allowedIP: false,
38+
},
39+
{
40+
url: "http://127.0.0.1:56789/test",
41+
disallowInternalIPs: true,
42+
allowedIP: false,
43+
},
44+
{
45+
url: "http://192.168.178.5:56789",
46+
disallowInternalIPs: true,
47+
allowedIP: false,
48+
},
49+
{
50+
url: "http://127.0.0.1:56789/foobar",
51+
disallowInternalIPs: true,
52+
allowedIP: true,
53+
},
54+
{
55+
url: "http://127.0.0.1:56789/glob/bar",
56+
disallowInternalIPs: true,
57+
allowedIP: true,
58+
},
59+
{
60+
url: "http://127.0.0.1:56789/glob/bar/baz",
61+
disallowInternalIPs: true,
62+
allowedIP: false,
63+
},
64+
{
65+
url: "http://127.0.0.1:56789/FOOBAR",
66+
disallowInternalIPs: true,
67+
allowedIP: false,
68+
},
69+
{
70+
url: "http://100.64.1.1:80/private",
71+
disallowInternalIPs: true,
72+
allowedIP: true,
73+
},
74+
{
75+
url: "http://100.64.1.1:80/route",
76+
disallowInternalIPs: true,
77+
allowedIP: false,
78+
},
79+
{
80+
url: "http://127.0.0.1",
81+
disallowInternalIPs: false,
82+
allowedIP: true,
83+
},
84+
{
85+
url: "http://localhost",
86+
disallowInternalIPs: false,
87+
allowedIP: true,
88+
},
89+
{
90+
url: "http://192.168.178.5",
91+
disallowInternalIPs: false,
92+
allowedIP: true,
93+
},
94+
{
95+
url: "http://127.0.0.1:80/glob/bar",
96+
disallowInternalIPs: false,
97+
allowedIP: true,
98+
},
99+
{
100+
url: "http://100.64.1.1:80/route",
101+
disallowInternalIPs: false,
102+
allowedIP: true,
103+
},
104+
}
105+
for _, tt := range testCases {
106+
t.Run(
107+
fmt.Sprintf("%s should be allowed %v when disallowed internal IPs is %v", tt.url, tt.allowedIP, tt.disallowInternalIPs),
108+
func(t *testing.T) {
109+
options := []ResilientOptions{
110+
ResilientClientWithMaxRetry(0),
111+
ResilientClientWithConnectionTimeout(50 * time.Millisecond),
112+
}
113+
if tt.disallowInternalIPs {
114+
options = append(options, ResilientClientDisallowInternalIPs())
115+
options = append(options, ResilientClientAllowInternalIPRequestsTo(
116+
"http://127.0.0.1:56789/foobar",
117+
"http://127.0.0.1:56789/glob/*",
118+
"http://100.64.1.1:80/private"))
119+
}
42120

43-
for i := 0; i < 10; i++ {
44-
for destination, passes := range map[string]bool{
45-
"http://127.0.0.1:" + port: false,
46-
"http://localhost:" + port: false,
47-
"http://192.168.178.5:" + port: false,
48-
allowedURL: true,
49-
"http://localhost:" + port + "/glob/bar": true,
50-
"http://localhost:" + port + "/glob/bar/baz": false,
51-
"http://localhost:" + port + "/FOOBAR": false,
52-
} {
53-
_, err := c.Get(destination)
54-
if !passes {
55-
require.Errorf(t, err, "dest = %s", destination)
56-
assert.Containsf(t, err.Error(), "is not a permitted destination", "dest = %s", destination)
57-
} else {
58-
require.NoErrorf(t, err, "dest = %s", destination)
59-
}
60-
}
121+
c := NewResilientClient(options...)
122+
_, err := c.Get(tt.url)
123+
if tt.allowedIP {
124+
assert.NotErrorIs(t, err, ssrf.ErrProhibitedIP)
125+
} else {
126+
assert.ErrorIs(t, err, ssrf.ErrProhibitedIP)
127+
}
128+
})
61129
}
62130
}
63131

‎httpx/ssrf.go

+4-15
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"net"
99
"net/http"
1010
"net/http/httptrace"
11-
"net/netip"
1211
"time"
1312

1413
"code.dny.dev/ssrf"
@@ -88,15 +87,10 @@ func init() {
8887
ssrf.WithAnyPort(),
8988
ssrf.WithNetworks("tcp4", "tcp6"),
9089
ssrf.WithAllowedV4Prefixes(
91-
netip.MustParsePrefix("10.0.0.0/8"), // Private-Use (RFC 1918)
92-
netip.MustParsePrefix("127.0.0.0/8"), // Loopback (RFC 1122, Section 3.2.1.3))
93-
netip.MustParsePrefix("169.254.0.0/16"), // Link Local (RFC 3927)
94-
netip.MustParsePrefix("172.16.0.0/12"), // Private-Use (RFC 1918)
95-
netip.MustParsePrefix("192.168.0.0/16"), // Private-Use (RFC 1918)
90+
ssrf.IPv4DeniedPrefixes...,
9691
),
9792
ssrf.WithAllowedV6Prefixes(
98-
netip.MustParsePrefix("::1/128"), // Loopback (RFC 4193)
99-
netip.MustParsePrefix("fc00::/7"), // Unique Local (RFC 4193)
93+
ssrf.IPv6DeniedPrefixes...,
10094
),
10195
).Safe
10296
allowInternalAllowIPv6 = otelTransport(t)
@@ -108,15 +102,10 @@ func init() {
108102
ssrf.WithAnyPort(),
109103
ssrf.WithNetworks("tcp4"),
110104
ssrf.WithAllowedV4Prefixes(
111-
netip.MustParsePrefix("10.0.0.0/8"), // Private-Use (RFC 1918)
112-
netip.MustParsePrefix("127.0.0.0/8"), // Loopback (RFC 1122, Section 3.2.1.3))
113-
netip.MustParsePrefix("169.254.0.0/16"), // Link Local (RFC 3927)
114-
netip.MustParsePrefix("172.16.0.0/12"), // Private-Use (RFC 1918)
115-
netip.MustParsePrefix("192.168.0.0/16"), // Private-Use (RFC 1918)
105+
ssrf.IPv4DeniedPrefixes...,
116106
),
117107
ssrf.WithAllowedV6Prefixes(
118-
netip.MustParsePrefix("::1/128"), // Loopback (RFC 4193)
119-
netip.MustParsePrefix("fc00::/7"), // Unique Local (RFC 4193)
108+
ssrf.IPv6DeniedPrefixes...,
120109
),
121110
).Safe
122111
t.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {

0 commit comments

Comments
 (0)
Please sign in to comment.