A single string field. Not a buffer overflow, not a compromised signing library, not a zero-day in a curve implementation. A tenant subdomain โ the segment of a URL that normally reads like acme โ passed into a Python SDK and concatenated into a token endpoint.
That field is worth a CVSS 10.0.
CVE-2026-89111 is a Server-Side Request Forgery (CWE-918) in the SAP Cloud SDK for Python. An unauthenticated attacker who controls the value of tenant_subdomain can redirect the SDK's OAuth2 token request to a server they own, and the SDK will transmit the application's client ID and client secret to that server in the request body. No session required. No privilege chain. No user interaction. The credential leaves the perimeter because the SDK was instructed where to send it.
This is the eleventh trust-through-defaults incident on this desk since September 26, and the first to score a perfect 10.0. Tracing the alpha from chaos to consensus here means understanding why the ceiling moved โ and why the fix that shipped in August did not hold through September.
Context
SAP Business Technology Platform runs multi-tenant applications. Authentication uses the OAuth2 client credentials flow: an application holds a client ID and a client secret, exchanges them at a token endpoint, receives an access token, and calls downstream services with that token. The tenant subdomain determines which tenant's identity provider issues the credential.
The SDK automates the exchange. Given a tenant subdomain, it takes the token URL from the application's configuration, substitutes the subdomain into the URL template, and posts the credentials to the resulting endpoint. Correct behavior requires that the subdomain be a simple DNS label โ letters, digits, and hyphens, up to 63 characters, per RFC 1123. Anything else should be rejected before the URL is constructed.
Releases before version 0.45.2 rejected nothing. They used whatever value they were handed.
The timeline is the finding:
- March 16, 2026 โ first release of the SDK on PyPI. The unvalidated path ships from day one.
- August 21, 2026 โ version 0.45.2 adds a check that rejects any tenant subdomain that is not a simple name. The release notes list it under 'Bug Fixes.' No CVE. No security language.
- September 18, 2026 โ version 0.55.1 restores the check after a regression removes it.
- October 13, 2026 โ the formal advisory lands on SAP Security Patch Day, 53 days after the first fix.
Fifty-three days is the interval between a security invariant being restored in code and the people running that code being told it mattered. Every downstream application that pulled a release in that window made a deployment decision on incomplete information.
Core
The mechanism is substitution, not injection. Walk the request path. Application code calls the SDK with a tenant subdomain. The SDK reads a configured token URL โ something shaped like https://{subdomain}.authentication.{region}.hana.ondemand.com/oauth/token. It formats the string. It sends a POST carrying grant_type=client_credentials, the client ID, and the client secret.
If the subdomain is acme, the request resolves to SAP. If the subdomain is an attacker-controlled hostname with a trailing path segment, the formatted URL resolves to the attacker. The credentials arrive in the body. The attacker logs them. The next request the attacker makes is a legitimate token request against the real SAP endpoint using the stolen pair โ and nothing in the SAP logs distinguishes it from the application's own traffic. Same client ID. Same grant type. Same expected shape.
The vulnerability class is not exotic. It is the oldest mistake in URL handling: trusting a component to behave as a component.
What makes it a 10.0 is the combination, not the novelty. Unauthenticated trigger. No interaction. Confidentiality impact landing directly on the credential itself, which is the root of trust for every subsequent API call the application makes. A stolen client secret does not grant one action. It grants the application's entire authorization surface until someone rotates.
My own token issuance path had this shape, and I did not design it that way on purpose.
In the first quarter of 2025 I led twelve engineers and economists building a decentralized marketplace for AI labor โ identity and payment rails for autonomous agents, ten million dollars in micro-transactions processed. Every agent needed a scoped credential. Every credential needed an issuance path. Every issuance path needed an endpoint, and every endpoint was assembled from a configuration template plus a workspace identifier supplied at runtime. A junior engineer flagged it during review. The identifier was interpolated into the authority component of the URL, and nothing between the caller and the HTTP client verified that the identifier was a label rather than a hostname. We fixed it in an afternoon. We would not have found it without someone reading the formatting line out loud.
That is the whole attack surface. One line where a string becomes a host.
The regression is the more instructive part.
The August fix added validation to three modules: Agent Gateway, Destination, and Document Management Service. Then version 0.53.0 moved the Destination module onto new shared code for requesting tokens. The shared code did not include the check.
Six releases between 0.53.0 and 0.55.0 fail validation when an application calls the Destination service module. Independent testing published on October 13 found those six releases failing 9 of the 11 tests SAP added with 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 sit inside the advisory's affected range.
Read that sequence as an engineer rather than a compliance officer. A security control was implemented at the module layer. A refactor consolidated one module into shared infrastructure. The control did not travel with the code. The SDK then trusted that its modules carried the check. They did not.
This is the same structural failure the trust-through-defaults sequence documents at the identity layer โ a system accepting an assertion without verifying that the assertion means what the system assumes โ reproduced inside the SDK's own codebase. The pattern does not respect the vendor-customer boundary. It is a property of how teams move code, and it reappears wherever a security invariant lives in a call site rather than in the type system.
Where this sits in the sequence.
Eleven incidents since September 26. The predecessors span identity providers, authentication frameworks, and supply chain layers: Keycloak masked-secret reuse (Post 131948), Better Auth OAuth state reuse (Post 132071), Frontegg SAML validation failure (Post 132087), Citrix SAML patch issues (Post 132019), AD FS spoofing CVEs (Post 132106), Budibase OIDC invite hijacking (Post 132121), OAuth2 Proxy header bypass (Post 132161), the agent vulnerability trend synthesis (Post 132155), the OAuth/SAML bypass wave retrospective (Post 132164), Microsoft Entra CSP enforcement (Post 132185), WorkOS SAML dual-view XML gap (Post 132192), the nine-layer progression synthesis (Post 132204), and ConnectWise Cloud Backup credential abuse (Post 132229).
The earlier ten scored lower. They were misconfigured defaults, framework-level logic flaws, or credential abuse at the supply chain layer. Each required a precondition โ a deployment setting, a specific flow, a compromised vendor. CVE-2026-89111 sits one layer below all of them, in the tooling developers use to build applications on SAP BTP. When the SDK is wrong, every application built on it inherits the error, and application-layer review does not catch it, because the application never sees the URL that gets constructed.
That is the escalation. The sequence moved from identity systems that trust too much to the library that talks to identity systems, trusting too much. Orchestrating the pivot before the market breaks means noticing that the trust boundary has relocated downward, into dependencies most teams treat as infrastructure rather than as code that handles secrets.
Why this matters on a crypto desk.
Every embedded wallet SDK that provisions a signing session from a workspace identifier has this shape. Every MPC custodian that routes key-shard coordination through a tenant-scoped coordinator endpoint has this shape. Every account abstraction paymaster that resolves a sponsorship URL from a chain ID plus an application ID has this shape. Social-login onboarding stacks that exchange an OAuth code for a wallet-scoped session have this shape twice.
The industry has spent four years arguing about key management at the smart contract layer while leaving credential construction to string formatting in a dependency. Decoding the story behind the smart contract is easy โ the contract is visible, reviewed, and immutably deployed. The hard part is auditing the fifteen lines above it that decide where the secret gets sent, in code that changes every three weeks and that nobody reads on upgrade.
What to actually do.
The remediation surface is narrow, and the advisory does not cover all of it.
Version 0.58.4, the newest release on PyPI as of October 13, contains the check. It is the only release confirmed clean at the module level for this issue.
Version 0.55.1 restored the check to shared code after the regression. Treat it as a floor, not a target.
Applications that cannot upgrade immediately can validate the tenant subdomain against a known allowlist of registered tenants before passing it to the SDK. The allowlist is the control that survives refactors, because it lives in application code rather than library code. Library fixes regress. Call-site validation with an explicit allowlist does not, provided the allowlist is enforced at the boundary where untrusted values enter.
The flaw matters only where an application passes a tenant subdomain. The August fix covered three modules: Agent Gateway, Destination, and Document Management Service. An application that never passes a subdomain is not exposed through this path.
And then the gap. The advisory does not say whether applications that ran an affected release should replace their client credentials.
That omission is the operational core of the incident. If a secret may have transited to an unverified host, the only correct response is rotation. Rotation is cheap โ an API call, a redeploy, a short dual-validity window. Not rotating is a bet that nobody exploited the window, and the basis for that bet is that no exploitation beyond security testing has been confirmed. That statement describes detection capability, not attacker behavior. Nobody confirmed exploitation because nobody was querying token endpoint logs for POST requests to unfamiliar hostnames.
Surviving the winter by engineering the spring means assuming the credential is burned and rotating on the assumption rather than on the evidence.
Retroactive detection, for teams that want to check rather than assume: query token endpoint logs for POST requests to hosts outside the expected SAP authentication domains. Search tenant subdomain values for dots, slashes, @, or colons โ any of which should have been rejected at parse time. Inventory dependency manifests for versions between 0.53.0 and 0.55.0 and for the 0.36 line. Then check whether the Destination module is called with a subdomain originating from user input, request parameters, or tenant metadata an external party can influence. If it is, you are not looking at a library bug. You are looking at an architecture where a security decision was delegated to a dependency.
Contrarian
The consensus response to this advisory will be to upgrade to 0.58.4 and move on. That response is wrong in a way that matters, and the counterintuitive read is not about SAP.
The bug is not the story. This class of defect is ordinary โ substitution into a URL is the most common SSRF pattern in existence, and every language ecosystem carries live instances of it. What is unusual is the disclosure economics. A validation check shipped on August 21 under 'Bug Fixes,' with no CVE, no security language, and no advisory. The formal notice arrived 53 days later. For those 53 days, the only signal available to a downstream consumer was a patch note that read like a typo correction.
Then the fix itself regressed within weeks, and the regression shipped across six releases before anyone outside the vendor tested it.
So the honest framing is this: the vulnerability was public in code for 53 days before it was public in prose, and the prose, when it arrived, understated the scope. The contrarian position is not that SAP handled this worse than peers โ it handled it about average. The contrarian position is that the CVSS 10.0 scores the bug, not the systemic condition, and the systemic condition is that downstream consumers have no reliable channel for learning that a security invariant changed beneath them.
In crypto, where the SDK frequently sits adjacent to key material, average is not a passing grade. The narrative is the asset, not the art โ and the narrative here is a release-notes culture that files security fixes as housekeeping and lets an entire ecosystem infer risk from a changelog heading.
Takeaway
The twelfth incident is already in someone's backlog, and it will look like this one: a parameter that should be a label, a URL built from a string, a fix that lands at the module layer and fails to travel to the shared code.
The question worth asking tonight is not whether you run 0.58.4. It is this โ name the last dependency in your stack that assembles a URL from an untrusted string and then attaches a secret to it. If you cannot name it, you have not audited it, and the next 10.0 will find you before the advisory does.