Skip to content

Commit 935d64c

Browse files
aeneasrory-bot
authored andcommitted
chore: tighten SSRF client detection with forbidgo
GitOrigin-RevId: dfeb7424d9ad1b80f9abf215fd8390ea04301f7e
1 parent 3a8b572 commit 935d64c

File tree

4 files changed

+55
-39
lines changed

4 files changed

+55
-39
lines changed

.golangci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ linters:
88
- ineffassign
99
- staticcheck
1010
- unused
11+
- forbidigo
1112
disable:
1213
- bodyclose # too many false negatives
1314

@@ -19,8 +20,20 @@ linters:
1920
- G306
2021
- G704
2122
- G705
23+
forbidigo:
24+
forbid:
25+
- pattern: "http\\.DefaultClient"
26+
msg: "Use reg.HTTPClient(ctx) or httpx.NewResilientClient() — http.DefaultClient bypasses SSRF protection and retry logic."
27+
- pattern: "retryablehttp\\.NewClient\\b"
28+
msg: "Use httpx.NewResilientClient() — retryablehttp.NewClient() bypasses SSRF protection and registry configuration."
2229
exclusions:
2330
rules:
2431
- linters:
2532
- staticcheck
2633
text: "SA1019" # we do use deprecated APIs on purpose sometimes
34+
- linters:
35+
- forbidigo
36+
path: "pkg/(httpclient|client-go)/"
37+
- linters:
38+
- forbidigo
39+
path: "_test\\.go$"

cmd/cliclient/client.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import (
1212

1313
"github.com/pkg/errors"
1414

15-
"github.com/hashicorp/go-retryablehttp"
16-
1715
"github.com/spf13/cobra"
1816

1917
"github.com/spf13/pflag"
2018

2119
kratos "github.com/ory/kratos/pkg/httpclient"
20+
"github.com/ory/x/httpx"
2221
)
2322

2423
const (
@@ -71,8 +70,9 @@ func NewClient(cmd *cobra.Command) (*kratos.APIClient, error) {
7170
}
7271

7372
conf := kratos.NewConfiguration()
74-
conf.HTTPClient = retryablehttp.NewClient().StandardClient()
75-
conf.HTTPClient.Timeout = time.Second * 10
73+
conf.HTTPClient = httpx.NewResilientClient(
74+
httpx.ResilientClientWithConnectionTimeout(10 * time.Second),
75+
).StandardClient()
7676
conf.Servers = kratos.ServerConfigurations{{URL: u.String()}}
7777
return kratos.NewAPIClient(conf), nil
7878
}

selfservice/strategy/password/validator.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ var (
5858
// password has been breached in a previous data leak using k-anonymity.
5959
type DefaultPasswordValidator struct {
6060
reg validatorDependencies
61-
Client *retryablehttp.Client
6261
hashes *ristretto.Cache[string, int64]
6362

6463
minIdentifierPasswordDist int
@@ -67,6 +66,7 @@ type DefaultPasswordValidator struct {
6766

6867
type validatorDependencies interface {
6968
config.Provider
69+
httpx.ClientProvider
7070
}
7171

7272
func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) (*DefaultPasswordValidator, error) {
@@ -81,9 +81,6 @@ func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) (*DefaultPas
8181
return nil, errors.Wrap(err, "error while setting up validator cache")
8282
}
8383
return &DefaultPasswordValidator{
84-
Client: httpx.NewResilientClient(
85-
httpx.ResilientClientWithConnectionTimeout(time.Second),
86-
),
8784
reg: reg,
8885
hashes: cache,
8986
minIdentifierPasswordDist: 5, maxIdentifierPasswordSubstrThreshold: 0.5,
@@ -123,7 +120,7 @@ func (s *DefaultPasswordValidator) fetch(ctx context.Context, hpw []byte, apiDNS
123120
if err != nil {
124121
return 0, err
125122
}
126-
res, err := s.Client.Do(req)
123+
res, err := s.reg.HTTPClient(ctx, httpx.ResilientClientWithConnectionTimeout(time.Second)).Do(req)
127124
if err != nil {
128125
return 0, errors.Wrapf(ErrNetworkFailure, "%s", err)
129126
}

selfservice/strategy/password/validator_test.go

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import (
1717
"testing"
1818
"time"
1919

20+
"github.com/hashicorp/go-retryablehttp"
2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"
2223

2324
"github.com/ory/herodot"
25+
"github.com/ory/kratos/driver"
2426
"github.com/ory/kratos/driver/config"
2527
"github.com/ory/kratos/pkg"
2628
"github.com/ory/kratos/selfservice/strategy/password"
@@ -30,6 +32,32 @@ import (
3032
"github.com/ory/x/httpx"
3133
)
3234

35+
// testRegistry embeds RegistryDefault and overrides HTTPClient to inject a
36+
// controllable HTTP client for HIBP-related tests.
37+
type testRegistry struct {
38+
*driver.RegistryDefault
39+
cl *retryablehttp.Client
40+
}
41+
42+
func (r *testRegistry) HTTPClient(_ context.Context, _ ...httpx.ResilientOptions) *retryablehttp.Client {
43+
return r.cl
44+
}
45+
46+
// newTestValidator wires base with a fake HIBP HTTP client and returns the
47+
// validator along with the fake client for request assertions and stubbing.
48+
func newTestValidator(t *testing.T, base *driver.RegistryDefault, opts ...httpx.ResilientOptions) (*password.DefaultPasswordValidator, *fakeHttpClient) {
49+
t.Helper()
50+
fakeClient := NewFakeHTTPClient()
51+
cl := httpx.NewResilientClient(append([]httpx.ResilientOptions{
52+
httpx.ResilientClientWithMaxRetry(1),
53+
httpx.ResilientClientWithConnectionTimeout(time.Millisecond),
54+
}, opts...)...)
55+
cl.HTTPClient = &fakeClient.Client
56+
s, err := password.NewDefaultPasswordValidatorStrategy(&testRegistry{RegistryDefault: base, cl: cl})
57+
require.NoError(t, err)
58+
return s, fakeClient
59+
}
60+
3361
func TestDefaultPasswordValidationStrategy(t *testing.T) {
3462
t.Parallel()
3563

@@ -93,16 +121,8 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
93121
t.Run("failure cases", func(t *testing.T) {
94122
t.Parallel()
95123

96-
_, reg := pkg.NewFastRegistryWithMocks(t)
97-
s, err := password.NewDefaultPasswordValidatorStrategy(reg)
98-
require.NoError(t, err)
99-
100-
fakeClient := NewFakeHTTPClient()
101-
s.Client = httpx.NewResilientClient(
102-
httpx.ResilientClientWithMaxRetry(1),
103-
httpx.ResilientClientWithConnectionTimeout(time.Millisecond),
104-
httpx.ResilientClientWithMaxRetryWait(time.Millisecond))
105-
s.Client.HTTPClient = &fakeClient.Client
124+
_, base := pkg.NewFastRegistryWithMocks(t)
125+
s, fakeClient := newTestValidator(t, base, httpx.ResilientClientWithMaxRetryWait(time.Millisecond))
106126

107127
t.Run("case=should send request to pwnedpasswords.com", func(t *testing.T) {
108128
ctx := contextx.WithConfigValue(t.Context(), config.ViperKeyIgnoreNetworkErrors, false)
@@ -139,12 +159,10 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
139159
t.Parallel()
140160

141161
const maxBreaches = 5
142-
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordMaxBreaches, maxBreaches))
143-
s, err := password.NewDefaultPasswordValidatorStrategy(reg)
144-
require.NoError(t, err)
162+
_, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordMaxBreaches, maxBreaches))
163+
s, fakeClient := newTestValidator(t, base)
145164

146165
hibpResp := make(chan string, 1)
147-
fakeClient := NewFakeHTTPClient()
148166
fakeClient.responder = func(req *http.Request) (*http.Response, error) {
149167
buffer := bytes.NewBufferString(<-hibpResp)
150168
return &http.Response{
@@ -154,8 +172,6 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
154172
Request: req,
155173
}, nil
156174
}
157-
s.Client = httpx.NewResilientClient(httpx.ResilientClientWithMaxRetry(1), httpx.ResilientClientWithConnectionTimeout(time.Millisecond))
158-
s.Client.HTTPClient = &fakeClient.Client
159175

160176
hashPw := func(t *testing.T, pw string) string {
161177
//#nosec G401 -- sha1 is used for k-anonymity
@@ -271,13 +287,8 @@ func TestChangeHaveIBeenPwnedValidationHost(t *testing.T) {
271287
testServerURL, err := url.Parse(testServer.URL)
272288
require.NoError(t, err)
273289

274-
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedHost, testServerURL.Host))
275-
s, err := password.NewDefaultPasswordValidatorStrategy(reg)
276-
require.NoError(t, err)
277-
278-
fakeClient := NewFakeHTTPClient()
279-
s.Client = httpx.NewResilientClient(httpx.ResilientClientWithMaxRetry(1), httpx.ResilientClientWithConnectionTimeout(time.Millisecond))
280-
s.Client.HTTPClient = &fakeClient.Client
290+
_, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedHost, testServerURL.Host))
291+
s, fakeClient := newTestValidator(t, base)
281292

282293
testServerExpectedCallURL := fmt.Sprintf("https://%s/range/BCBA9", testServerURL.Host)
283294

@@ -291,13 +302,8 @@ func TestChangeHaveIBeenPwnedValidationHost(t *testing.T) {
291302
func TestDisableHaveIBeenPwnedValidationHost(t *testing.T) {
292303
t.Parallel()
293304

294-
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedEnabled, false))
295-
s, err := password.NewDefaultPasswordValidatorStrategy(reg)
296-
require.NoError(t, err)
297-
298-
fakeClient := NewFakeHTTPClient()
299-
s.Client = httpx.NewResilientClient(httpx.ResilientClientWithMaxRetry(1), httpx.ResilientClientWithConnectionTimeout(time.Millisecond))
300-
s.Client.HTTPClient = &fakeClient.Client
305+
_, base := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyPasswordHaveIBeenPwnedEnabled, false))
306+
s, fakeClient := newTestValidator(t, base)
301307

302308
t.Run("case=should not send request to test server", func(t *testing.T) {
303309
require.NoError(t, s.Validate(t.Context(), "mohutdesub", "damrumukuh"))

0 commit comments

Comments
 (0)