← Back to all reports

A Tenant Login Token Unlocks the Staff-Only Admin API and Other Tenants' Data

Reported Jun 30, 2026
Severity High
Platform Web / API
Vulnerability Class OAuth Audience Confusion (CWE-863)
Target Type Enterprise Procurement / Supply-Chain SaaS
Impact Cross-tenant data and internal staff details exposed

The Risk

A staff-only control panel, meant to be opened only by the company's own employees, could be reached using an ordinary customer's normal login. From there, a single search box handed back records belonging to other customers of the platform, including well-known pharmaceutical and technology brands: their account details, links to their uploaded files, and even the company's own internal staff email addresses and computer addresses. Any customer who could log into the regular app could do this with a handful of everyday search words, and the whole thing was read-only, so nothing needed to be broken or guessed. It is the equivalent of a customer's front-door key quietly opening the staff-only back office and every other customer's filing cabinet.

The Vulnerability

The platform runs two separate applications. The tenant-facing Admin App (admin.example-saas.com) is the ordinary console every customer logs into. The staff-only Sysadmin App (sysadmin.example-saas.com) is the internal operator console used to provision new customer tenants, and it is meant for company staff only.

The Sysadmin App's sign-in enforces that boundary correctly. Its login is locked to company Google single sign-on: the OAuth authorize request forces identity_provider=Google and redirects to accounts.google.com. There is no username/password form. Only a staff Google identity can obtain a sysadmin session, and an ordinary customer has no path into this application through its own interface.

The problem is at the API layer behind that console. The Sysadmin API accepts an OAuth ID token minted by the other, lower-trust application. A normal login at the tenant Admin App produces a session token whose properties clearly mark it as tenant-level and untrusted for staff use:

  • Its audience (aud) is the tenant app's client (TENANT_APP_CLIENT), not the sysadmin app's client (SYSADMIN_APP_CLIENT).
  • It was obtained via native username and password, not federated through Google (it carries no identities claim).
  • Its user type claim is Tenant, scoped to a single customer tenant.

Despite all three signals, the Sysadmin API accepts this token as an authenticated principal. This is classic audience confusion: an API trusting a token that was issued for a different application entirely.

The Attack

The entire attack is reproducible in a browser with developer tools. No external tooling is required.

Obtaining a Tenant Token

Log into the tenant Admin App at admin.example-saas.com as an ordinary customer. The login sets the tenant session token as an httpOnly cookie. Because it is httpOnly, page scripts cannot read it, but the developer tools Application tab exposes its value directly. Decoding the token confirms it is a tenant-level token: the audience is the tenant app's client, the user type is Tenant, and there is no federated-identity claim.

Presenting It to the Staff-Only API

Navigating the browser tab onto the sysadmin origin makes the next request same-origin, avoiding the cross-origin wall that would otherwise block a request issued from the tenant tab. From a developer-tools console on that origin, the tenant token is presented to the sysadmin API as the Authorization header:

const ID_TOKEN = 'PASTE_TENANT_TOKEN_HERE';
const r = await fetch('/admin/jobs/search?name=test', {
  headers: { Authorization: ID_TOKEN }
});
const d = await r.json();
console.log('status', r.status, 'records', d.length);

The response is 200 OK with background-job records. Crucially, the returned records belong to tenants other than the attacker's own. On this one job-search route there is no tenant-scope check and no permission check, so it returns records across the entire platform.

Proving the Boundary Exists Everywhere Else

What confirms this is a genuine gap and not intended access is that the exact same token is rejected on every other sysadmin operation. The all-tenants list returns Forbidden: User does not have required permissions. The sysadmin user list, the entity inspector, and the tenant-access and impersonation reports return the same. The tenant-scoped job route returns Forbidden: different tenantId for any other tenant. The API therefore treats this token as an authenticated-but-restricted principal, and the job-search endpoint is the lone route missing the scope and permission checks.

The Impact

A handful of ordinary search terms surfaced records spanning more than a dozen distinct customer tenants, including well-known pharmaceutical and technology brands. Each foreign record exposed a broad set of sensitive data belonging to other customers and to the platform operator:

  • Foreign tenant identifiers and job identifiers
  • Customer administrator email addresses
  • Internal cloud storage paths, including the original filenames of uploaded documents
  • Internal staff email addresses
  • Staff source IP addresses
  • Impersonation chains showing which staff member acted on which tenant
  • Internal container and pod names revealing infrastructure naming

This breaks two boundaries at once. It breaches the application trust boundary, because a staff-only console is reachable by a non-staff principal. And it breaches tenant isolation, because one customer can read another customer's data. The exposure is read-only, but it scales directly with the number of search terms tried, so a patient attacker could enumerate a large share of the platform's tenants.

One nuance worth noting: the accepted token being granted broad in-tenant roles influences only which records the search returns. It is not what enables the boundary crossing. Acceptance or rejection happens at the authentication layer, based on the token's issuer, audience, and identity source, before any role is evaluated. No tenant-app token, regardless of its role within its own tenant, should authenticate to the staff-only Sysadmin API at all.

Remediation

  • Validate token audience at the Sysadmin API. Reject any token whose audience is the tenant Admin App's client rather than the sysadmin client.
  • Validate identity source. Accept only the Google-federated staff identities the Sysadmin App requires, and reject native username/password tokens outright.
  • Add the missing tenant-scope and permission checks to the job-search route so it matches every other job and sysadmin route.
  • Adopt a default-deny posture on the Sysadmin API so a newly added route inherits the scope and permission gate rather than starting open.
  • Audit access logs for prior use of tenant-audience tokens against the Sysadmin API and rotate any exposed internal credentials or paths surfaced by the leak.