client: allow injecting a requests.Session + stop following redirects

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.
This commit is contained in:
2026-06-04 12:41:12 -04:00
parent a7fb0fb887
commit 5410b283e2
2 changed files with 24 additions and 3 deletions

View File

@@ -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)

View File

@@ -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