From 5410b283e2875d1f84a6735c1897367d113e0402 Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Thu, 4 Jun 2026 12:41:12 -0400 Subject: [PATCH] client: allow injecting a requests.Session + stop following redirects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP Session was hardcoded with no injection point and followed redirects by default, so a server-side caller pointing the client at an attacker-influenced base_url (a tenant's prime_endpoint) had no way to attach an SSRF policy, and a public endpoint could 302-redirect the request to an internal address (e.g. the cloud metadata service). - Client/DragonchainSDK now accept an optional `session` so callers can inject a Session whose transport adapter refuses internal IPs. Default stays unguarded for trusted/CLI use — the guard belongs in the server. - Requests are sent with allow_redirects=False; Prime never legitimately redirects, and a 3xx now surfaces to the caller instead of being followed. --- prime_sdk/__init__.py | 7 ++++++- prime_sdk/client.py | 20 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/prime_sdk/__init__.py b/prime_sdk/__init__.py index 334afd0..1eecae1 100644 --- a/prime_sdk/__init__.py +++ b/prime_sdk/__init__.py @@ -91,6 +91,7 @@ class DragonchainSDK: auth_key_id: str, auth_key: str, base_url: str, + session: "Optional[requests.Session]" = None, ): """Create a new Dragonchain SDK client. @@ -99,8 +100,12 @@ class DragonchainSDK: auth_key_id: Your authentication key ID. auth_key: Your authentication key. base_url: Base URL of your node (e.g. "https://chains.dragonchain.com"). + session: Optional pre-configured ``requests.Session``. Inject one + with an SSRF-guarded transport adapter when ``base_url`` is + attacker-influenced (a tenant's prime_endpoint); the default + session is unguarded and intended for trusted/CLI use. """ - self._client = Client(public_id, auth_key_id, auth_key, base_url) + self._client = Client(public_id, auth_key_id, auth_key, base_url, session=session) self.transaction = TransactionClient(self._client) self.transaction_type = TransactionTypeClient(self._client) self.contract = ContractClient(self._client) diff --git a/prime_sdk/client.py b/prime_sdk/client.py index 017b31c..b35fea2 100644 --- a/prime_sdk/client.py +++ b/prime_sdk/client.py @@ -39,13 +39,20 @@ class Client: auth_key: str, base_url: str, timeout: int = DEFAULT_TIMEOUT, + session: Optional[requests.Session] = None, ): self.public_id = public_id self.auth_key_id = auth_key_id self.auth_key = auth_key self.base_url = base_url.rstrip("/") self.timeout = timeout - self._session = requests.Session() + # A caller may inject a pre-configured Session — e.g. one with a + # transport adapter that refuses to connect to internal IPs — when the + # base_url is attacker-influenced (a tenant's prime_endpoint). The + # default Session is unguarded, which is fine for trusted/CLI use; the + # SSRF policy belongs in the server that points this client at + # untrusted endpoints, not baked into the client library. + self._session = session if session is not None else requests.Session() # -- introspection helpers (mirror the Go accessors) -------------------- @@ -120,8 +127,17 @@ class Client: headers["Content-Type"] = content_type url = self.base_url + path + # allow_redirects=False: Prime is a JSON API that never legitimately + # 3xx-redirects, and following redirects is an SSRF vector (a public + # endpoint that 302s to an internal address). A redirect surfaces as a + # 3xx the caller can inspect rather than being silently followed. resp = self._session.request( - method, url, data=body_bytes, headers=headers, timeout=self.timeout + method, + url, + data=body_bytes, + headers=headers, + timeout=self.timeout, + allow_redirects=False, ) resp_text = resp.text