← Back to all reports

Cross-Tenant Decryption on Shared Master Key: Any Tenant Reads Any Other Tenant's Secrets

Reported Apr 29, 2026
Severity Critical
Platform Web / API
Vulnerability Class Broken Access Control (CWE-284)
Target Type Enterprise SaaS / Digital Adoption
Impact Cross-tenant secret disclosure

The Risk

Every paying customer of this platform shared a single master lock that protected all stored secrets. Any customer could ask the system to unlock any other customer's secrets and the system handed them over in plain text. That meant integration passwords, login secrets, and signing keys belonging to one customer were readable by anyone else with a normal account on the platform. No special role, no admin access, just two normal accounts and a few API calls.

The Vulnerability

The platform's identity service exposed two endpoints, POST /kms/encrypt and POST /kms/decrypt, that proxied straight through to the cloud provider's key management service. Every other endpoint in the same service correctly returned 403 Forbidden resource for a regular tenant token, but these two returned 201 Created and ran the requested operation. The role guard simply was not applied to the encrypt and decrypt pair.

The proxy used a single global customer master key with no per-tenant EncryptionContext. Every ciphertext it returned began with the same fifty-character prefix, which encodes the master key ID. Identical prefix across tenants confirmed one global key shared by every customer on the platform. A caller-supplied keyId or encryptionContext was silently ignored.

That combination, a missing role guard plus a shared key with no binding context, broke the cross-tenant trust boundary. Any value the platform encrypted at rest under this key (identity provider secrets, OAuth client secrets, integration bearer tokens, SAML signing keys, anything routed through the encrypt endpoint) was recoverable by any other tenant who could obtain the ciphertext bytes.

The Attack

The chain ran end-to-end with two ordinary tenant accounts on the platform's QA environment. Each account had its own tenant ID, confirmed by decoding the JWT payloads.

  1. Tenant A authenticated and obtained a normal bearer token through the standard login flow.
  2. Tenant A called POST /kms/encrypt with a known plaintext. Response: 201 Created with a base64 ciphertext.
  3. Tenant B, a completely separate tenant, posted that ciphertext to POST /kms/decrypt using its own bearer token.
  4. Response: 201 Created containing tenant A's plaintext, byte-for-byte.

The same chain ran in reverse: tenant B encrypted, tenant A decrypted. Both directions worked. There was no rate limit and no plaintext-size restriction beyond the underlying provider's native four-kilobyte limit.

Why this was a missing guard, not by design

Sibling endpoints in the same service (/accounts, /users, /idp, /sso, /tenants, /me) returned {"message":"Forbidden resource","statusCode":403} for the same tenant token. The application-layer role guard existed and worked everywhere else. The encrypt and decrypt pair were the only endpoints in the service exposed to external tenant traffic, and the API documentation labeled them as intended for internal service-to-service use only.

The Impact

As any authenticated tenant on the platform, an attacker could:

  • Decrypt any ciphertext minted by the identity service for any other tenant, recovering the original plaintext byte-for-byte. With access to ciphertext bytes from any cross-tenant data leak, log file, or backup, the attacker recovered other tenants' identity provider secrets, OAuth client secrets, integration bearer tokens, and SAML signing keys.
  • Encrypt arbitrary data under the platform's production-class master key, attributing the attacker's plaintext to the platform's own cloud principal in audit logs and burning shared key-management quota.
  • Break the cross-tenant trust boundary using two normal accounts. No admin role, no backoffice token, no privileged scope.

For an enterprise SaaS provider whose customers store federation secrets and integration credentials in the platform, this is a complete failure of multi-tenant isolation. The fix scope is narrow but the blast radius is the entire customer base.

Remediation

  • Apply the same application-layer role guard that protects the rest of the service to POST /kms/encrypt and POST /kms/decrypt. External tenant tokens should not reach either endpoint.
  • Bind every encrypt call to a per-tenant EncryptionContext (for example {"tenantId":"<id>"}) and require a matching context on every decrypt call. This blocks cross-tenant decryption even if the role guard is bypassed in the future.
  • Stop exposing the cloud key-management service as a public proxy through the API gateway. Internal services that need encryption should call the cloud provider directly using internal IAM, not through a tenant-reachable HTTP path.
  • Audit cloud-provider key usage logs for cross-tenant decrypt patterns during the exposure window.