SAP Cloud SDK SSRF: a subdomain that sends your secret

$ cat blog/sap-cloud-sdk-ssrf-oauth-credentials.md
✓ incident
✓ SSRF
✓ secrets
✓ authentication
6 min read
$ 

SAP has disclosed CVE-2026-89111, a server-side request forgery (SSRF) flaw rated CVSS 10.0 in the SAP Cloud SDK for Python. Disclosed on October 13, 2026, it let an attacker who controls a tenant subdomain make the SDK send an application's OAuth2 client ID and client secret to a server of the attacker's choosing. The Hacker News has the report.

What happened

The SDK reads an OAuth token URL from configuration and substitutes the tenant's subdomain into it. According to the report, releases without validation used the subdomain exactly as given, then sent the application's client ID and client secret to the resulting URL. An attacker who controls the subdomain can make that URL point at their own server and capture the client credentials.

The preconditions are specific: a multi-tenant application on SAP BTP that uses the SDK, and an attacker who controls the tenant subdomain the application passes to it.

On versions, the report says:

  • All versions below 0.45.2 are affected, including 0.36.1 and 0.36.2. The 0.36 line never received the fix.
  • Version 0.45.2 (August 21) added validation that "accepts only a single name made of letters, digits and hyphens, up to 63 characters long."
  • Version 0.55.1 (September 18) added the same check to the shared token code. A subdomain containing URL delimiters "now raises ValueError immediately instead of silently routing credentials to an attacker-controlled endpoint."
  • In The Hacker News' own testing, 0.53.0 through 0.55.0 skip the check for Destination calls, and 0.55.0 also for the Document Management Service.
  • Safe versions are 0.45.2 through 0.52.2, and 0.55.1 through 0.58.4.

The flaw existed since the SDK's first PyPI release on March 16. The August release notes listed the fix under "Bug Fixes", and the advisory came 53 days after the fix. SAP credits the GitHub user SunandM. There are no known attacks.

Why it worked

Most SSRF write-ups are about reading internal data, such as a cloud metadata endpoint. This one is the other direction: the request carries a secret, so wherever it lands, the secret goes too. If any part of the destination comes from input, the input decides who receives your credentials.

Building a URL by string substitution makes this easy to get wrong. A "subdomain" is supposed to be one DNS label, but a string can contain /, ?, #, @ or ., and each of those changes how the URL is parsed: a different host, userinfo, or a path that moves the real host out of the way. A check that looks for bad characters will miss one. A check that accepts only what a label can contain will not.

Here is an illustrative example in Python, not the SDK's code. The vulnerable version substitutes and sends:

# illustrative example, not the product's code (vulnerable)
TOKEN_URL = "https://{subdomain}.auth.example.com/oauth/token"

def fetch_token(subdomain: str) -> dict:
    url = TOKEN_URL.format(subdomain=subdomain)          # input shapes the host
    resp = requests.post(url, data={
        "grant_type": "client_credentials",
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],    # goes wherever url points
    })
    return resp.json()

The fixed version allowlists the shape, checks the tenant is one you registered, and confirms the parsed host before sending anything:

# illustrative example, not the product's code (fixed)
LABEL = re.compile(r"[A-Za-z0-9-]{1,63}")
AUTH_SUFFIX = ".auth.example.com"

def fetch_token(subdomain: str) -> dict:
    if not LABEL.fullmatch(subdomain) or subdomain not in registered_tenants():
        raise ValueError("unknown tenant subdomain")
    url = TOKEN_URL.format(subdomain=subdomain)
    parsed = urllib.parse.urlsplit(url)
    if parsed.scheme != "https" or parsed.hostname != subdomain.lower() + AUTH_SUFFIX:
        raise ValueError("token URL host mismatch")
    resp = requests.post(url, allow_redirects=False, data={
        "grant_type": "client_credentials",
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],
    })
    return resp.json()

Three habits make this class hard to reintroduce:

  • Allowlist, don't blocklist. Accept exactly the characters and length the value can legally have.
  • Parse, then compare the host. Check the URL a parser produces, not the string you built.
  • Don't follow redirects with credentials attached. A token endpoint has no reason to redirect.

Note the version history too: in The Hacker News' testing, some code paths in 0.53.0 to 0.55.0 skipped the check again. Putting validation in one shared function that every caller goes through, as 0.55.1 does, is what keeps it from drifting.

attack, as reported where OpenRouting checks 1 Attacker controls a tenant subdomaintenant data, not code 2 App passes it to the SDK✓ SSRF agent: tainted URL 3 SDK builds the token URL as giveninside a dependency 4 Client ID and secret POSTed there✓ Secrets agent: data exposure 5 Attacker captures OAuth credentialsoutside a code scan
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

  1. Upgrade the SDK to a safe version: 0.45.2 through 0.52.2, or 0.55.1 through 0.58.4. If you're on the 0.36 line, move off it; it never got the fix. Avoid 0.53.0 through 0.55.0 if you use Destination calls (or, on 0.55.0, the Document Management Service).
  2. If you can't upgrade yet, compare each tenant subdomain against your list of registered tenants before passing it to the SDK, as the report recommends.
  3. Rotate the OAuth client secret if a multi-tenant app ran an affected version and you can't rule out an attacker-controlled subdomain.
  4. Audit your own code for any place a URL that carries credentials, such as a token endpoint, a webhook with an auth header or a signed callback, is built from tenant or user input.
  5. Restrict egress where you can, so credential-bearing requests can only reach the identity provider hosts you expect.

How OpenRouting helps

OpenRouting scans your own code, so it looks for this pattern where you build URLs yourself:

  • Semgrep's SSRF and tainted-URL rules flag HTTP calls whose target is built from request data, in Python and other languages.
  • The [SSRF agent](/agents/server-side-request-forgery) reviews those findings: whether the destination is fixed, allowlisted or user-controlled, and whether redirects and internal address ranges are handled. A URL fixed in config is marked a likely false positive.
  • The [secrets & data exposure agent](/agents/secrets-data-exposure) reviews credentials crossing a boundary unsafely, and Semgrep's secret rules flag client secrets committed in the files you scan. Secret values are redacted before any AI call.
  • AI triage (Claude, via Amazon Bedrock) explains each real finding in plain English and suggests a fix, and PR review runs the same checks on the files a pull request changes.

What it won't catch

  • The SDK version you depend on. OpenRouting doesn't do dependency scanning yet, so it won't flag that your requirements pin an affected release. Check your lockfile and SAP's advisory.
  • Flows through a third-party library. Taint rules may not follow a value from your handler into a library's internals, so passing a tenant subdomain to an SDK may not be flagged even when the SDK builds the URL unsafely.
  • Your SAP BTP tenants. OpenRouting doesn't inspect a provider's platform or your tenant registry.
  • Findings need a scan. Results appear when you run a scan or review a pull request.

Sources

Questions

What is SAP Cloud SDK for Python CVE-2026-89111?

It is a server-side request forgery flaw rated CVSS 10.0. Releases without validation substituted a tenant subdomain into the OAuth token URL as given, so an attacker who controls that subdomain could make the SDK send the application's client ID and client secret to their own server.

Which SAP Cloud SDK for Python versions are safe?

Safe versions are 0.45.2 through 0.52.2, and 0.55.1 through 0.58.4. All versions below 0.45.2 are affected, the 0.36 line never received the fix, and in The Hacker News' testing 0.53.0 through 0.55.0 skip the check for Destination calls.

Was CVE-2026-89111 exploited?

There are no known attacks. The preconditions are specific: a multi-tenant application on SAP BTP that uses the SDK, and an attacker who controls the tenant subdomain the application passes to it.

How do I stop credentials going to a URL built from input?

Allowlist the input's shape, such as a single DNS label, and check it against your registered tenants. Then parse the final URL and compare its host before sending anything, and don't follow redirects with credentials attached.

Keep reading