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