Home/Case studies/Payments API
API penetration test · Fintech

One missing check exposed every tenant's payment mandates

The most damaging finding of the engagement was not a clever exploit chain. It was a single endpoint that trusted an identifier the client supplied.

Illustrative write-up. Details are composited and anonymised across engagements of this type. Figures are representative rather than drawn from one named customer, and nothing here identifies a client.

The context

A Series B payments platform in the UK, around forty engineers, processing direct debit collections on behalf of business customers. Their product is multi-tenant: every customer sees only their own mandates, payments and payers. That isolation is the entire product promise, and it is enforced almost everywhere in the codebase.

They came to us because a prospective enterprise customer had asked for an independent API test before signing. The brief was straightforward: test the public API the way a hostile paying customer would, with two valid accounts on the platform and no source code.

What we were looking for

When an API is multi-tenant, the interesting question is rarely whether authentication works. It almost always does. The interesting question is whether authorisation is checked on every single object the API will hand back, on every endpoint, including the ones nobody thinks about.

That class of flaw has a name, Broken Object Level Authorisation, and it sits at number one in the OWASP API Security Top 10 for a reason: it is invisible to scanners, it does not look like a bug in code review, and it is trivially exploitable once found.

What we found

The platform's main mandate endpoints were solid. Requesting another tenant's mandate by its identifier returned a clean 403 across every route we tried. Someone had clearly thought about this.

The gap was in an export endpoint added later for a reporting feature. It accepted a mandate reference as a query parameter, looked the mandate up directly, and returned it. The authentication middleware confirmed you were a valid, logged-in user. Nothing confirmed the mandate belonged to you.

  • Any authenticated customer could read any other customer's mandate, including payer name, sort code, masked account number and collection schedule
  • Mandate references were sequential, so enumerating the entire customer base took minutes rather than requiring a guess
  • The same identifier was accepted by a related update route, which meant collection amounts and dates could be altered, not just read
  • Nothing in the application logs distinguished this from ordinary reporting traffic
The controls were not missing because the team was careless. They were missing because the endpoint was added six months after the pattern was established, by someone who had inherited the pattern rather than written it. Engagement lead

Why it mattered

Read access alone would have been a serious breach of the platform's core promise and a reportable personal data incident under UK GDPR. Write access made it materially worse: an attacker could alter the amount and date of a collection against a payer who had never dealt with them.

We rated it critical, not because the technique was sophisticated, but because the exploitation path was short, the blast radius was every tenant on the platform, and detection was effectively zero.

How it was fixed

The engineering fix took under a day. The tenant scoping already existed as a shared helper; the export route simply had not used it. The team applied the helper, then went looking for every other route that took an object reference from the client.

  1. The vulnerable route was scoped to the authenticated tenant and deployed within twenty-four hours
  2. Two further routes with the same shape were found internally and fixed in the same change
  3. Sequential mandate references were replaced with opaque identifiers, so enumeration is no longer trivial even if a check is missed again
  4. An integration test now asserts that a second tenant receives a 403 on every object route, so a future endpoint cannot quietly reintroduce it

We retested four days after delivering the report. The critical was closed, the two related routes were confirmed fixed, and we reissued the report and the attestation letter at no additional cost.

The pattern worth stealing: the fix that mattered was not the code change, it was the test. A missing authorisation check will happen again on a growing codebase. An automated assertion that a second tenant always gets a 403 turns it from a breach into a failed build.

What changed afterwards

The enterprise deal signed. More usefully, the team's definition of done for any new endpoint now includes the tenant isolation test, which means the next engineer to inherit the pattern inherits the guardrail with it.

The other findings in the report were routine by comparison: verbose error messages leaking stack traces, a missing rate limit on the login route, and inconsistent enforcement of a spending cap. Worth fixing, none of them business-ending.

Find out before your customers do

We will test one scoped target and write up a single verified finding to this standard, free, with no sales call first.