A login helper exposes the sidecar’s identity
CVE-2026-76969 affects the multitenancy and extensibility component @sap/cds-mtxs in SAP’s Cloud Application Programming Model (CAP). This component manages tenant-specific application extensions and can run as a separate companion service, called an mtx sidecar, or inside the application. Its login helper accepts an unauthenticated request without either supported credential input and obtains an access token using the sidecar’s own client credentials. The handler returns that privileged token to the caller.
The exposure requires an affected release with extensibility enabled, authentication configured to use XSUAA, SAP’s authorization service, and UAA client credentials available to the component. The attacker must be able to reach POST /-/cds/login/token. No existing account, access token, passcode, or user interaction is required. The subsequent tenant-modification chain targets an already subscribed tenant and uses extension activation with CSV resources.
Missing inputs select the wrong OAuth grant
The login route is registered in the ExtensibilityService served callback in srv/extensibility-service.js. It attaches directly to CAP’s Express web application, outside the service definitions protected by @requires, CAP’s role-based access restriction. The service’s authorization rules therefore do not protect this helper.
An OAuth grant specifies how a token request establishes its authority. This handler selects a refresh-token grant when the caller supplies refresh_token, or a password grant when the caller supplies passcode. If neither field is present, it leaves the grant form empty instead of rejecting the request. It then requests the <xsappname>.cds.ExtensionDeveloper scope and calls authToken with the configured credentials and any caller-supplied subdomain. A scope is a named permission carried by a token; xsappname identifies the application’s scope prefix.
The call reaches token() in lib/utils.js. That helper defaults grant_type to client_credentials, an OAuth flow that authenticates an application client rather than a user. It sets client_id to the sidecar’s configured client ID and authenticates to UAA with the sidecar’s client ID and client secret in an HTTP Basic authorization header. Thus, missing caller credentials trigger use of the server’s own credentials.
On success, the login handler parses the UAA response and returns its JSON, including access_token, to the anonymous caller. The exposed credential is the resulting token; the described response does not disclose the raw client secret. The failed boundary is the transition from an unauthenticated login request to issuance of a credential representing the trusted application client.
CAP’s JWT middleware, which processes JSON Web Tokens, maps application scopes to roles by removing the xsappname prefix. It also grants internal-user to a client_credentials token whose client ID matches the configured client ID. The returned token consequently supplies the cds.ExtensionDeveloper role and the internal-user authorization needed to access the extensibility service. In the observed behavior, unauthenticated access to GET /-/cds/extensibility/Extensions was rejected, while access using the returned token succeeded.
Behavioral evidence covers @sap/cds-mtxs 4.0.2 with @sap/cds 9.9.3 and the SQLite deployment plugin. Token issuance and client-identity role mapping used a simulated identity provider; real XSUAA issuance and signature verification were not tested. The vulnerable route and default-grant logic were in the unmodified target package.
How the token enables tenant changes
The set action at POST /-/cds/extensibility/set accepts extension definitions, CSV resources, a tag, an activation mode, and a target tenant. Definitions can use CDL, CAP’s model-definition language, or CSN, its structured model representation. The CSV resources supply data for deployment.
Ordinarily, ExtensibilityService.set uses the tenant associated with the request. When the caller has internal-user, it instead accepts a caller-supplied tenant. The leaked client identity therefore crosses a second boundary: the attacker can select a subscribed tenant rather than being confined to the request’s tenant.
The action passes the extension and CSV resources through put /Extensions into the selected tenant’s cds_xt_Extensions store. JobsService queues activateExtension, which invokes DeploymentService.extend(tenant, options). On the SQLite path, srv/plugins/sqlite.js calls cds.deploy with automatic schema evolution and the supplied CSV resources. Schema evolution changes the database structure to match the extension. CSV deployment uses UPSERT semantics: it inserts new rows or updates existing rows with matching keys. Attacker-controlled CSV content can therefore replace existing business data.
Tenant schema and model changes were observed through service interfaces. Direct database inspection in a separate equivalent SQLite instance confirmed replacement of all existing rows in the tested table. This establishes a concrete data-integrity impact, but does not establish blanket replacement of every table or arbitrary database contents. The HANA backend was not behaviorally tested.
The demonstrated modification scope is already subscribed tenants. Modification of the default provider tenant did not succeed: an empty tenant caused an ASSERT_MANDATORY error with HTTP 400, and _getExtensions4 rejected activation without a tenant value. Code paths also support potential tenant-data deletion through DeploymentService.unsubscribe, whose SQLite implementation removes the tenant database, and extension removal through discard or DELETE operations in ExtensibilityService. Those destructive paths were not executed. Systematic extraction of tenant business data was not established; confirmed disclosure concerns the access token and job registry, with some schema and model access also demonstrated.
A separate job-registry exposure
The same internal token authorizes POST /-/cds/jobs/enqueue, allowing attacker-controlled task.tenant metadata to enter job records. Separately, the Jobs and Tasks projections in srv/jobs-service.cds have @readonly and @cds.persistence.exists annotations but no @requires restriction. Read-only access limits modification through those projections; it does not require authentication.
As a result, anonymous requests to GET /-/cds/jobs/Jobs can retrieve job records, including metadata inserted through privileged enqueueing. This is an ancillary information-disclosure path. Anonymous job reads alone do not establish tenant business-data modification.
Affected releases and the corrective check
The declared affected ranges include four release branches. Behavioral testing was limited to 4.0.2; the 1.x, 2.x, and 3.x ranges come from the CVE record.
| Branch | Declared affected versions | Fix evidence |
|---|---|---|
| 1.x | Through 1.18.3 | 1.18.4 is an inferred first fixed release; the missing-input rejection check was not verified. |
| 2.x | Through 2.7.6 | 2.7.7 is an inferred first fixed release; the missing-input rejection check was not verified. |
| 3.x | Through 3.9.6 | The missing-input rejection check is confirmed in 3.9.7. |
| 4.x | Through 4.0.2 | The missing-input rejection check is confirmed in 4.0.3. |
Upgrade to a release containing the check that rejects login-grant requests when both passcode and refresh_token are absent. The corrected handler uses POST /-/cds/login/grant and returns HTTP 400 for that case, preventing the helper from falling back to the sidecar’s client_credentials grant. This check is confirmed in 3.9.7 and 4.0.3.
In 4.0.3, the old POST /-/cds/login/token route redirects to the new handler with HTTP 308. For 1.18.4 and 2.7.7, only registration of the new route and redirection from the old route are confirmed; neither the rejection check nor the redirect status was verified. Their identification as first fixed releases is inferred from release adjacency and endpoint changes. A route change alone is not evidence that the missing-input check is present. The described login fix does not establish that anonymous job-registry access was corrected.
Signals to investigate
A successful unauthenticated response from POST /-/cds/login/token containing an access_token, when the request supplied neither passcode nor refresh_token, is the reported signature of the token-disclosure flaw. Such a response establishes token exposure, but does not by itself show subsequent tenant modification.
For possible follow-on activity, review unexpected activated extensions, new tenant schema fields, and business rows overwritten through extension CSV resources. These effects are consistent with the modification chain and need to be assessed against authorized extension activity.
Anonymous HTTP 200 responses containing job records from GET /-/cds/jobs/Jobs indicate the separate registry disclosure. Unexpected tenant metadata in those records can also indicate job insertion, but job records alone do not prove compromise of tenant business data.