From bc2b62287320d1c01d1163186ef7da1d045c2178 Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Thu, 4 Jun 2026 12:32:33 -0400 Subject: [PATCH] client: allow injecting a custom *http.Client (SSRF-guardable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NewClient hardcoded its *http.Client, so a server-side caller making requests to an attacker-influenced baseURL (a tenant's prime_endpoint) had no way to attach an SSRF policy — the transport followed redirects and dialed any resolved IP, reachable being the cloud metadata service. Add NewClientWithHTTPClient + NewDragonchainSDKWithHTTPClient so callers can supply a client whose transport enforces a dial-time resolved-IP guard and redirect policy. Existing constructors delegate with the prior default (30s timeout), so this is backward compatible — the guard itself lives in the consuming server (e.g. brill-api/pkg/prime), not in this client lib. --- client/client.go | 23 ++++++++++++++++------- dragonchain.go | 12 +++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/client/client.go b/client/client.go index 929e43a..5d537f1 100755 --- a/client/client.go +++ b/client/client.go @@ -23,14 +23,23 @@ type Client struct { } func NewClient(publicID, authKeyID, authKey, baseURL string) *Client { + return NewClientWithHTTPClient(publicID, authKeyID, authKey, baseURL, nil) +} + +// NewClientWithHTTPClient is like NewClient but uses a caller-supplied +// *http.Client — e.g. one whose transport is SSRF-guarded, or one with a +// non-default timeout. A nil hc falls back to the package default (30s +// timeout, default transport), so existing callers are unaffected. +func NewClientWithHTTPClient(publicID, authKeyID, authKey, baseURL string, hc *http.Client) *Client { + if hc == nil { + hc = &http.Client{Timeout: 30 * time.Second} + } return &Client{ - publicID: publicID, - authKeyID: authKeyID, - authKey: authKey, - baseURL: strings.TrimSuffix(baseURL, "/"), - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, + publicID: publicID, + authKeyID: authKeyID, + authKey: authKey, + baseURL: strings.TrimSuffix(baseURL, "/"), + httpClient: hc, } } diff --git a/dragonchain.go b/dragonchain.go index 56f58c8..eb2e5c0 100755 --- a/dragonchain.go +++ b/dragonchain.go @@ -50,6 +50,8 @@ package sdk import ( + "net/http" + "git.dragonchain.com/dragonchain/prime-sdk-go/block" "git.dragonchain.com/dragonchain/prime-sdk-go/client" "git.dragonchain.com/dragonchain/prime-sdk-go/contract" @@ -80,7 +82,15 @@ type DragonchainSDK struct { // Returns a configured SDK client ready to make API calls. // All API methods on the returned client require a context.Context parameter. func NewDragonchainSDK(publicID, authKeyID, authKey, baseURL string) *DragonchainSDK { - c := client.NewClient(publicID, authKeyID, authKey, baseURL) + return NewDragonchainSDKWithHTTPClient(publicID, authKeyID, authKey, baseURL, nil) +} + +// NewDragonchainSDKWithHTTPClient is like NewDragonchainSDK but routes every +// request through the caller-supplied *http.Client. Pass a client whose +// transport enforces an SSRF policy (guarded dialer + redirect checks) when +// the baseURL is attacker-influenced. A nil hc falls back to the SDK default. +func NewDragonchainSDKWithHTTPClient(publicID, authKeyID, authKey, baseURL string, hc *http.Client) *DragonchainSDK { + c := client.NewClientWithHTTPClient(publicID, authKeyID, authKey, baseURL, hc) return &DragonchainSDK{ client: c, Transaction: transaction.NewTransactionClient(c),