Skip to content
Case study

One Missing Check, Total Exposure: How a Single API Flaw Exposed Every Record Across 3 Apps

One number, changed in a request, returned a different member’s file. Then it let us edit that file, close it, and download the identity documents attached to it. It worked identically from the web app, the iOS app and the Android app, because the check that should have stopped it was missing from the one thing all three of them share - the API underneath.

7 min read
The system
A membership platform. Members sign in to raise and track cases with the organisation, attach supporting documents, and download documents the organisation issues to them.
In scope
Three client applications - web, iOS and Android - and the one API all of them call.
What was at risk
Every member record on the platform: case correspondence, the identity documents attached to it, and the documents the organisation issues to named individuals. Readable, editable and deletable by anybody holding an ordinary account.

Anonymised. An assessment carried out by our testers, with client-identifying detail withheld under the terms of the engagement. Full statement

What we were given

A membership organisation ran one product across three client applications - a web application, an iOS app and an Android app - and one REST API that all three of them called. Members signed in to raise cases with the organisation, track them, attach supporting documents, and download documents issued to them individually.

The data was ordinary and sensitive in the way most member data is: correspondence about individuals’ circumstances, identity documents attached in support, and files that exist specifically to say something official about one named person.

All three applications and the API were in scope, which is less common than it should be. The usual version of this engagement covers the web application because that is the thing the buyer can see, and the scope conversation treats the mobile apps as a separate project for a later quarter.

Getting to the traffic

Both mobile apps pinned their certificates, so the first task was seeing what they sent. On a device under our control this took standard tooling and part of an afternoon.

Pinning is a speed bump

Certificate pinning stops an attacker on the network reading a user’s traffic, which is worth having. It does not stop the person holding the phone from reading their own traffic, because they control the device the check runs on. Any control implemented in the client is negotiable by whoever owns the client. Treat pinning as friction, and put the decisions on the server.

What the traffic showed was unremarkable and is the reason the rest of this happened: plain REST calls, each carrying an identifier for the record being acted on, with the signed-in member’s token in the header.

What we found

Changing the identifier to one belonging to a different member returned that member’s record. The server checked that the caller was signed in. It never checked that the record belonged to them.

That is authorisation bypass through a user-controlled key, and it is the single most common serious finding we report. The application had authentication and no authorisation: it knew who you were and did not use it.

It was not only reads

The same substitution worked on every action the interface offered. Another member’s record could be updated. It could be closed. Its attachments could be downloaded. And the document the organisation issues to an individual member, the one whose entire purpose is to be evidence about that person, could be retrieved by anyone able to work out an identifier.

Identifiers of this kind are rarely hard to work out, and "hard to guess" was never the control anyway. They enumerate when they are sequential, and they leak through shared links, exports and support tickets even when they are not.

Why it appeared in all three

Because the check was missing in the API, it was missing everywhere. The web application, the iOS app and the Android app were three front doors onto the same open room. The mobile findings and the web findings were not four problems that happened to look similar. They were one problem, seen from four places.

Two more that mattered

Signing out did not sign you out

Logging out cleared the application’s local state and left the session valid on the server. A token captured before logout kept working afterwards. On a shared, sold or lost device that turns "I logged out" into a false statement, and it is the failure mode people are least prepared for because the interface told them the opposite. There is more on this in the guide on what logout has to do server-side.

Unlimited password reset emails

The password reset endpoint would send mail to a chosen address with no cap. That is not a compromise on its own. It is a way to make a real person’s inbox unusable at the moment they need it, and a way to make a phishing message arriving in the middle of that flood look expected. The rate limiting guide covers the trap in fixing this badly.

How far it went

One missing check exposed every member record on the platform, on every surface, for reading and for modification. There was no privilege escalation to chain, no exploit to write, no unusual condition to arrange. An ordinary member account and a changed number in a request was the whole of it.

The commercially important part is what would have happened with a narrower scope. Test only the web application and you find the flaw, fix it in the web tier, and leave two mobile apps calling the same unprotected API while believing the problem is closed. Test only the mobile apps and you get the same result the other way around. The finding is cheap to fix once and expensive to fix three times, and only one of those outcomes leaves you actually fixed.

The fix

  1. Authorisation moved into the API, checked per record, on every action rather than on reads alone. The question the server has to answer is not "is this caller signed in" but "does this caller own this record".
  2. Sessions invalidated server-side at logout, so the token stops working when the user says it should.
  3. Password reset limited by source and by destination address, with the destination cap failing slowly rather than failing closed.
  4. The document download path treated as a distinct authorisation decision rather than a file served from a known location.

Verification matters more than usual on this one. A fix applied at a single call site looks identical, from the outside, to a fix applied in the right place, and the difference only shows on the application nobody retested. Our engagements include a retest of the findings once they are fixed, and the retest here has to repeat the substitution in all three applications, not in the one where it was first reported. How we run an engagement has the sequence.

The findings, in one place

  • The same authorisation flaw appeared in all three applications, because the check belonged to the API underneath them and the API never made it.
  • Certificate pinning delayed the assessment by an afternoon. It is a speed bump, not an access control, and it should not be counted as one.
  • The flaw worked on writes as well as reads: another member’s record could be updated, closed and its attachments downloaded.
  • Signing out cleared the app and left the session valid on the server, so a returned or lost device stayed signed in.
  • A web-tier fix would have left the mobile apps exposed and the client believing the problem was solved.

What to check on your own system

If your web application and your mobile apps talk to the same API, you do not have three applications to secure. You have one, with three ways in. Scope the API. The clients in front of it are worth testing for what only they can get wrong, which is real but smaller: data left on the device, controls implemented locally, secrets shipped in the bundle. The decisions live on the server, and so do the findings that end up mattering.

Confidentiality

Anonymised. This describes an assessment carried out by our testers. The client, the dates, the sector detail and any figure that could identify the organisation are withheld or generalised under the confidentiality terms of the original engagement, and no endpoint, parameter or payload from the assessment appears here.

FAQ

Questions this raises

Would a scanner have found this?

A scanner can flag that an endpoint accepts an identifier. It cannot tell you that the record behind that identifier belongs to somebody else, because it does not know who anybody is or what they should be able to reach. Authorisation flaws need two accounts and a person who knows what the application is for. This is the clearest line between automated scanning and an assessment.

Does testing three applications cost three times as much?

No, and this engagement is why. Most of the work is understanding the API and the authorisation model, and that is done once. The additional cost of the mobile clients is the device work and the client-side checks specific to them. Scoping all of it together is materially cheaper than three separate engagements, and it is the only version that produces one coherent answer.

Is certificate pinning worth implementing at all?

Yes, for the threat it addresses: an attacker positioned on the network between your user and your service. What it does not do is stop the owner of the device inspecting their own traffic, so it must never be the reason a control is left off the server. Implement it, and assume it is bypassed when you decide what the API will accept.

How do you test authorisation without breaking real data?

With test accounts, in an environment agreed before the engagement starts, and with the destructive actions demonstrated once against records we created. That is part of what the rules of engagement settle before any testing begins.

Is the same true of yours?

Most of what is above came from ordinary accounts and ordinary requests. A 30-minute scoping call establishes what would be worth checking on your system, what it costs, and when we could start.