On October 13, SAP published a security advisory for CVE-2026-89111. CVSS 10.0. Server-side request forgery, CWE-918. Unauthenticated. The flaw lets an attacker hijack OAuth2 token requests in the SAP Cloud SDK for Python by manipulating the tenant_subdomain parameter, redirecting the credential exchange to a server they control and harvesting application client IDs and secrets in transit.
The score is not the story. The timeline is.
The validation check that would have blocked this shipped on August 21, inside version 0.45.2. It was filed under "Bug Fixes." No CVE in the release notes. No advisory. No mention of security. The formal disclosure landed 53 days later, on SAP Security Patch Day.
Fifty-three days is not a disclosure lag. It is an exposure window with a marketing problem. Teams that upgraded in that window read "Bug Fixes," shrugged, and moved on. Teams that didn't upgrade stayed exposed and never knew. And a subset of teams that did upgrade stayed exposed anyway, because the check did not survive a refactor.
That last part is the one worth your attention. Not the SSRF. The regression.
What the SDK is actually doing
The SAP Cloud SDK for Python handles authentication for multi-tenant SAP Business Technology Platform applications. The model is standard client credentials: an application holds a client ID and a client secret, and exchanges them for an access token. Nothing exotic. This is the same grant type that sits underneath half the enterprise integrations on the planet.
The multi-tenant twist is the tenant_subdomain parameter. BTP applications serve many tenants from one deployment. When an application needs a token for a specific tenant, it passes that tenant's subdomain to the SDK. The SDK takes the token URL from the application's configuration, substitutes the subdomain value into that URL, and posts the client credentials to the result.
Read that sequence again. The SDK does not validate the subdomain. It takes a string from the caller, splices it into a URL, and then sends a secret to whatever address comes out the other end. In releases without a validation check, that is precisely what happened. Whatever value was supplied went into the URL.
So the attacker's job is trivial. Supply a subdomain that resolves the constructed URL to infrastructure they own. The SDK does the rest, on schedule, with the credentials attached. No authentication required on the attacker's side. No interaction with the victim beyond the initial request. The secret arrives at the attacker's endpoint because the application asked the SDK to deliver it there.
This is why CWE-918 fits and why the score is a clean 10.0. Confidentiality is total: client secrets in plaintext at the attacker's server. Integrity is total: a stolen client credential issues tokens that the platform will honor. Availability follows. No privileges needed, no user interaction, no complexity beyond DNS.
And the blast radius is not one application. It is every tenant the compromised credential can reach. Multi-tenant means multi-tenant in both directions.
The sequence, and why this one is different
This is the eleventh trust-through-defaults incident on this desk since September 26. The list is long enough to be a pattern rather than a coincidence: Keycloak masked-secret reuse, Better Auth OAuth state reuse, Frontegg SAML validation failure, Citrix SAML patch issues, AD FS spoofing CVEs, Budibase OIDC invite hijacking, OAuth2 Proxy header bypass, Microsoft Entra CSP enforcement, WorkOS SAML dual-view XML gap, ConnectWise Cloud Backup credential abuse, plus two synthesis pieces mapping the progression.
The first ten scored lower. Not because they were less damaging in practice, but because of where they lived. Misconfigured defaults. Framework-level logic flaws. Credential abuse at the supply chain layer. Each one required a specific deployment shape to be exploitable.
CVE-2026-89111 sits one layer deeper. It hits the foundational tooling developers use to build applications on BTP. Not a product you configure. A library you import. The distinction matters because library-level flaws inherit into every application built on top, and they inherit silently. There is no admin console where you can see the flaw. There is a requirements file.
And the vulnerability has existed since the SDK's first release on PyPI, March 16, 2026. Not a bad patch. Not a rushed feature. The original code.
The check, and the refactor that ate it
The August 21 fix was correct in isolation. Version 0.45.2 added a validation that rejects any tenant subdomain that is not a simple name: a single label composed of letters, digits, and hyphens, up to 63 characters, per RFC 1123. No dots. No scheme. No path traversal. No colon-and-port. That rule is the right shape. It makes URL splicing safe by making the spliced value structurally incapable of escaping the host.
It was applied to three modules: Agent Gateway, Destination, and Document Management Service.
Three modules. Not one shared function.
Version 0.53.0 then moved the Destination service module onto new shared code for requesting tokens. The migration was architectural housekeeping, the kind of change that ships in every mature SDK. The shared code did not include the check.
The security invariant did not travel with the module. It stayed behind in the old call path, guarding a code route that was no longer being used.
Testing published on October 13 confirmed the damage. Six releases between 0.53.0 and 0.55.0 fail the validation check when an application calls the Destination service module. Those six releases failed 9 of the 11 tests SAP added alongside version 0.55.1. Version 0.55.1, released September 18, restored the check to the shared code. Two older releases on the 0.36 line, 0.36.1 and 0.36.2, also lack the check and fall inside the advisory's affected range. Version 0.58.4, the newest release on PyPI as of October 13, contains it.
Sit with the 9-of-11 number. SAP wrote eleven tests to prove the fix held. Six consecutive releases broke nine of them. The tests existed. The regression happened anyway, because the tests were written against modules and the invariant lived in a call path.
This is the same structural failure the trust-through-defaults sequence documents at the identity layer, now appearing inside the SDK's own source tree. A system trusts an assertion without verifying that the assertion still means what the system assumes. The SDK trusted that its modules carried the check. They did not. The auditors trusted that three patched modules meant one patched invariant. They did not.
Volatility is where the signal lives. Here the signal is not the CVE. It is that a security control was implemented three times instead of once, and the fourth implementation, written later by someone else, had no way to know the control existed.
Why a crypto desk should care about an SAP SDK
I run a quant trading desk. I do not run SAP. So let me be direct about why I spent a day on this.
Institutional crypto does not live in a vacuum. Custody, treasury, settlement, and reconciliation sit inside enterprise systems. When I negotiated direct APIs with three major custodians in 2024 to cut settlement from T+2 to T+0, the integration surface was not a wallet. It was an enterprise identity stack issuing tokens to services that moved eight figures. The credential was the position. Whoever held the token held the inventory.
The SAP Cloud SDK for Python is exactly that layer for BTP-hosted applications. If a firm runs multi-tenant crypto-adjacent services on BTP, or runs reconciliation and treasury dashboards there, or has a fintech partner whose integration terminates there, then this CVE is in their dependency graph whether or not anyone on the trading floor has heard of it.
The exploit economics are unpleasant. An attacker who captures a client ID and secret from a token request gets an identity the platform will honor. No phishing. No session riding. No malware. Just a DNS record and patience. From that token, the reachable surface is whatever the service principal can touch: configuration, tenant metadata, and any downstream API the application was authorized to call.
Based on my audit experience mapping exit paths across twelve wallets during the Terra collapse, I can tell you what credential theft looks like from the inside. It looks like nothing. There is no anomalous transaction to flag, no unusual gas pattern, no mempool signature. Theft of an API credential produces only authorized-looking requests, and authorized-looking requests are indistinguishable from normal traffic until someone reconciles intent against logs. By the time you reconcile, the data has moved twice.
That is the asymmetry. On-chain, every movement is public and permanent, which is why forensic work is possible at all. Off-chain, credential abuse leaves a clean audit trail written by the attacker's own token. You are not investigating a break-in. You are investigating a guest who used the front door correctly.
The contrarian read: the vulnerability is not the vulnerability
Here is where I will disagree with most of the coverage this week.
The consensus take will be: patch to 0.58.4, validate your subdomains, move on. That advice is correct and it is also the least interesting thing in the advisory.
The actual risk is inventory. Most organizations affected by CVE-2026-89111 cannot answer a simple question: which of our services pass tenant_subdomain into this SDK, and which release of the SDK is each one pinned to? That question cannot be answered from a security dashboard. It requires reading dependency lockfiles across every Python service in the estate. I have run that exercise. It takes days, it produces an ugly spreadsheet, and it is the only thing that converts a CVSS 10.0 from an abstract threat into a work item.
Second disagreement: the operational gap the advisory leaves open. SAP does not say whether applications that ran a release without the check should replace their client credentials. Read that again. The advisory documents a vulnerability that exfiltrates client secrets, then stays silent on whether those secrets should be considered burned.
This is the part that separates a compliance response from a security response. A compliance response reads the advisory, patches to 0.58.4, closes the ticket. A security response assumes every credential that transited an unvalidated code path between March 16 and September 18 is compromised and rotates it. The second response costs a maintenance window and a lot of coordination. The first response costs nothing and buys nothing.
Rotating is the only action that invalidates an attacker's position after the fact. Patching closes the door. Rotation evicts whoever is already inside. Liquidity dries up faster than hope, and so does an access token once it is in someone else's log.
Third disagreement, and this one is about how the industry prices this class of failure. The release notes called a security fix a bug fix. That is not a documentation oversight. That is a signal-suppression event. Any desk that models counterparty operational risk should be pricing the difference between an organization that ships CVE-tagged releases and one that ships "Bug Fixes." The first gives you a patch cadence you can model. The second gives you a 53-day blind spot you cannot.
I have spent nine years building systems that assume the counterparty is rational but the infrastructure is not. This advisory is a clean example of the second. The SDK was not malicious. It was confident. It believed its modules carried the check because three of them did.
What I would actually do this week
Pin the SDK to 0.58.4 or later. Do it before anything else. If your dependency resolver is fighting you, force the pin and fix the fallout afterward.
Then grep. Every Python service that references tenant_subdomain is in scope. Every service that imports the Destination module is in scope twice. Confirm which of the affected windows each one sat in: any release on the 0.36 line at 0.36.1 or 0.36.2, and any release between 0.53.0 and 0.55.0.
Then decide on rotation. My default is rotate. If a service principal held a client secret while running an unvalidated code path, that secret has an unknown exposure history. The cost of rotation is a scheduled window. The cost of not rotating is an attacker with a valid token and no reason to announce themselves.
Then add the allowlist. Applications that cannot upgrade immediately should validate the tenant subdomain against a known list of registered tenants before it ever reaches the SDK. This is defense in depth, not a fix. But it is the difference between a library bug and a breach.
And then, the part almost nobody will do: write the invariant test. Not a module test. A test that asserts, for every code path that constructs a token URL, that the constructed URL's host matches the configured host. Three modules got the check in August. One shared function should have gotten it. If you build multi-tenant services, that test is worth more than the next three features on your roadmap.
Don't trade the dip; trade the volume. The volume here is eleven trust-through-defaults incidents in under a month. That is not a run of bad luck. That is an industry systematically shipping authentication code that trusts its inputs, then discovering the inputs were never trustworthy.
The next one is already in someone's release notes, filed under Bug Fixes, waiting 53 days. The question is not whether your stack is affected. The question is whether you would know.