Back to Case Studies
FinTech / SaaS
Web Application VAPT
2026

One Number Off: Cross-Tenant IDOR in a Small Invoicing SaaS Exposed Every Customer's Invoices

Redacted (B2B Invoicing SaaS, ~12 employees)

5 days

idor
bola
broken access control
multi-tenancy

Critical

Severity

~12,000

Invoices Exposed

480

Tenants Affected

48 hours

Remediation

On This Page

Key outcome

Cross-tenant IDOR identified, scoped, and remediated within 48 hours. Tenant-aware authorisation introduced application-wide, not only on the affected route.

Table of Contents

13

Executive Summary

A small B2B SaaS invoicing product, identified throughout this document only as Redacted, engaged XHack for a focused web application VAPT in advance of a planned funding round. The product had grown organically from a side project into a paid SaaS used by approximately 480 small businesses, primarily freelancers, accountants, and small agencies, to issue invoices to their own clients.

XHack identified a cross-tenant Insecure Direct Object Reference (IDOR) on the invoice download endpoint within the first day of testing. The endpoint accepted a numeric invoice identifier in the URL and returned the associated PDF. It verified that the requesting user was authenticated. It did not verify that the requesting user belonged to the tenant that owned the invoice.

A standard authenticated user on any tenant could iterate through invoice IDs and download the PDF for any invoice in the system, including invoices belonging to entirely unrelated businesses. Approximately 12,000 invoices were exposed by this single missing check.

No real data was exfiltrated during the test. Findings were disclosed within hours of confirmation, and the client patched the affected endpoint and applied a wider audit-driven fix across the rest of the application within 48 hours.

Client Background

The client is a small SaaS company with twelve employees. Their product helps very small businesses generate, send, and track invoices. Their customer base is concentrated in the freelancer and small-agency segment. Most tenants have between one and five users and process between ten and a few hundred invoices per month.

The client cannot be identified beyond this description for commercial reasons.

Scope

  • In scope: the production web application and its public API, accessed as a standard paying customer
  • Out of scope: Stripe integration internals, infrastructure, internal admin tooling
  • Approach: grey-box. XHack received two test tenants on the production application with realistic dummy data and standard user accounts on each.

Discovery

XHack began with attack-surface mapping. The application followed a typical SaaS pattern: authenticated users land on a dashboard scoped to their tenant, and most read endpoints accept identifiers in the URL.

Mid-morning on day one, while reviewing the invoice download flow on tenant A, XHack noted that the URL pattern looked like the following:

GET /api/invoices/19402/download
Cookie: session=<tenant-A-user-session>

The response was a PDF of invoice number 19402, owned by tenant A. As expected.

XHack then logged in as a standard user on tenant B, copied the session cookie for that account, and issued a request for the same invoice ID:

GET /api/invoices/19402/download
Cookie: session=<tenant-B-user-session>

The response was a 200 OK with the PDF of invoice 19402, the invoice belonging to tenant A, returned in full to a session that had no relationship to tenant A.

The IDs were sequential integers. The application did not check tenant ownership. With a valid login on any tenant, a user could request invoice 1, invoice 2, invoice 3, and so on, and receive the PDF for each.

Verifying the Scope

XHack scripted a careful, ethical scope verification with the client's explicit consent and a strict request budget. The script issued one request per second for one hundred invoice IDs sampled across the integer space, recording only the HTTP status and the size of the response. It did not store any returned PDFs.

Of the 100 sampled IDs, 94 returned valid PDFs from a tenant other than the requesting tenant. Six returned 404, indicating either deleted invoices or unallocated IDs. The total invoice ID space at the time of the test ran from 1 to approximately 12,400, indicating that on the order of 12,000 invoices were technically exposed by the same flaw.

Vulnerability Properties

Property Detail
Vulnerability Class Insecure Direct Object Reference (Broken Object-Level Authorization)
OWASP Top 10 A01:2021 Broken Access Control
CWE CWE-639 Authorization Bypass Through User-Controlled Key
CVSSv3 Score 9.1 (Critical) — Confidentiality: High, Integrity: None, Availability: None
Attack Vector Network
Authentication Required Yes (any standard tenant user)
Privilege Required Low
User Interaction None
Impact Cross-tenant invoice disclosure across the entire customer base

Why It Happened

The application's data model included a tenant_id column on the invoices table from day one. The query that backed the download endpoint, however, was a leftover from an early version of the product when there was only one tenant. It read, in effect:

SELECT pdf_blob FROM invoices WHERE id = :invoice_id

The code had been written before multi-tenancy was added. When multi-tenancy arrived, the team migrated the data model and the dashboard queries, but a handful of older endpoints, including the download endpoint, were never updated. They continued to look up invoices by primary key alone.

The dashboard UI never exposed an arbitrary invoice ID input, so during normal use a tenant only ever saw their own invoices. The flaw was invisible from the front end. It became visible the moment anyone hand-crafted a request, which is exactly what an attacker does.

Impact Assessment

For the client and their customers, the realistic impacts were severe:

  • Confidentiality breach across the entire customer base. Invoices contain client names, billing addresses, line-item descriptions, amounts, and frequently bank or VAT details on the issuer side. Twelve thousand invoices is not a small leak.
  • Reportable personal data exposure. Many invoices contained personal data of the issuing freelancer's own clients. Under GDPR Article 33, an exploited version of this would likely have triggered mandatory notification within 72 hours.
  • Reputational impact disproportionate to size. Small SaaS products competing for trust with established players cannot afford a public disclosure of "any customer could read any other customer's invoices."
  • Funding-round risk. Disclosure during due diligence would have been materially negative for the client's planned raise.

There was no evidence of prior exploitation in the application's access logs. The IDs were sequential, but no log entries showed a pattern consistent with enumeration.

Remediation

Immediate (Same Day)

  • Add tenant scoping to the download query. The query was changed to require both the invoice ID and the requesting user's tenant ID, returning 404 Not Found when there is no match. Returning 404 rather than 403 avoids leaking the existence of an invoice that the user is not authorised to see.
  • Apply a global authorisation middleware to all /api/invoices/* routes that loads the invoice by ID, checks tenant ownership, and rejects unauthorised access at the framework level rather than relying on each individual handler.

Short-Term (Within 48 hours)

  • Audit every other route in the API for the same pattern. Three additional routes (an export endpoint, a payment-status endpoint, and an attachment endpoint) were found to have the same missing check and were patched in the same release.
  • Replace sequential integer IDs with random, high-entropy identifiers for all newly created invoices going forward. This does not fix the underlying authorisation flaw, but it reduces the value of any future enumeration if a similar flaw is ever reintroduced.

Longer-Term

  • Tenant context as a first-class concern in the framework. Every database query in the codebase now flows through a thin wrapper that requires a tenant context to be present, refusing to execute otherwise. This is a structural fix that prevents new endpoints from forgetting tenant scoping.
  • Automated authorisation regression tests in CI. A dedicated test suite issues cross-tenant requests against every authenticated route on every pull request. The build fails if any cross-tenant access succeeds.
  • Periodic third-party VAPT on a defined cadence, given the data sensitivity of the product and the small in-house engineering team.

Outcome

The client patched the original endpoint within four hours of disclosure and the three additional endpoints within 48 hours. The wider tenant-context wrapper and CI authorisation suite were deployed within two weeks.

A re-test by XHack confirmed that all four affected endpoints now correctly return 404 Not Found for any cross-tenant request, that the new tenant-aware data layer rejects any query missing a tenant context, and that the CI suite reliably catches cross-tenant regressions before they reach production.

The client's funding round proceeded on schedule. No customer-facing incident was disclosed because no real-world exploitation had occurred and no real data had been accessed beyond the small, agreed sample taken during the assessment.

This case is unremarkable in its mechanics. IDOR is the oldest and most common access-control flaw on the web. It remains the leading entry in the OWASP Top 10 in 2026 because the conditions that produce it, organic codebases, evolving data models, and missing structural authorisation, are present in almost every small SaaS company at some point in its life. The fix is rarely difficult. The honest assessment is rarely commissioned.

Engagement details

Client

Redacted (B2B Invoicing SaaS, ~12 employees)

Industry

FinTech / SaaS

Service

Web Application VAPT

Duration

5 days

Year

2026

Tags
idor
bola
broken access control
multi-tenancy
saas security
web application security
owasp top 10
vapt
Get a Web App VAPT

Start your engagement

Get a Web App VAPT

XHack delivers the same rigorous methodology behind every case study. Let us pressure-test your defences.

Get a Web App VAPT