Technical finding · Web application & API assessment
Cross-tenant invoice disclosure via broken object-level authorization
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NDescription
The GET /api/invoices/{invoice_id} endpoint fails to enforce object-level authorization between tenants. An authenticated user can retrieve an invoice belonging to another organization by substituting the invoice_id path parameter. The endpoint returns the requested invoice’s billing contact, outstanding balance and currency without verifying that the requesting user is authorized to access the owning organization.
- Affected asset
https://billing.example.test- Endpoint
GET /api/invoices/{invoice_id}- Input vector
invoice_idpath parameter
Risk
An authenticated attacker with a valid target invoice identifier can access another tenant’s billing information. This bypasses tenant isolation and exposes customer billing contacts and outstanding balances to users outside the owning organization. Exploitation requires a standard user account and a valid invoice identifier; no interaction from the affected customer is required. The demonstrated impact is unauthorized read access to invoice data.
Remediation
Enforce server-side object-level authorization before returning invoice data. Derive the requesting user’s tenant context from a validated session and verified membership records, then constrain invoice retrieval by both the requested identifier and the authorized tenant. Verify that the user holds invoice-read permission within that tenant. Reject requests outside the permitted scope with HTTP 403 Forbidden, or a consistent HTTP 404 Not Found response where object existence must be concealed.
Apply the authorization policy to all invoice operations, including listing, download, export and modification. Add regression tests for authorized access, cross-tenant requests, users without invoice-read permission and unauthenticated requests. Randomized invoice identifiers do not replace authorization checks.
Reproduction steps
-
Establish the account and invoice ownership
Use
USER-A, a standard user with invoice-read permission inORG-Aand no membership or shared billing access inORG-B. Obtain two invoice identifiers from the respective organizations:INV-A-101, owned byORG-A, andINV-B-104, owned byORG-B. Authenticate asUSER-Aand retain the session token for both requests below. -
Retrieve an authorized invoice
Request
INV-A-101using theUSER-Atoken. The endpoint returnsHTTP 200 OKand an invoice associated withORG-A.E-01 · Authorized invoice retrieval GET /api/invoices/INV-A-101 HTTP/1.1 Host: billing.example.test Authorization: Bearer <USER_A_TOKEN> Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { "invoice_id": "INV-A-101", "organization_id": "ORG-A", "billing_contact": "billing@customer-a.example", "balance_due": "320.00", "currency": "USD" } -
Request another tenant’s invoice
Replay the request, changing only the path identifier to
INV-B-104. Keep the sameUSER-Atoken and request headers. The endpoint returnsHTTP 200 OKand the billing information forORG-B.E-02 · Cross-tenant invoice retrieval GET /api/invoices/INV-B-104 HTTP/1.1 Host: billing.example.test Authorization: Bearer <USER_A_TOKEN> Accept: application/json HTTP/1.1 200 OK Content-Type: application/json { "invoice_id": "INV-B-104", "organization_id": "ORG-B", "billing_contact": "billing@customer-b.example", "balance_due": "2400.00", "currency": "USD" } -
Verify the authorization bypass
The second response contains
organization_id: ORG-Band that organization’s invoice data, although the request uses a credential authorized only forORG-A. The expected result is an authorization denial with no invoice data returned. The successful response confirms cross-tenant read access through the user-controlled invoice identifier.