Skip to content
Case study

Beyond the Login Screen: The FinTech Flow That Left Balances Unprotected After Sign-In

Once a session existed, this application never asked for anything again. No second factor at the transfer, no confirmation tied to the payment, no limit on attempts, and a PIN short enough to work through rather than guess. Every control sat at the front door, so a live session was all that stood between whoever was holding the phone and somebody else’s balance.

7 min read
The system
A mobile payments application on iOS and Android, with a supporting web interface. Users authenticate, hold a balance, and transfer money.
In scope
Both mobile applications, the web interface, and the transaction flows behind them.
What was at risk
Any balance on the platform. An unlocked phone, a borrowed device or a recovered session token was sufficient to complete a transfer, because after sign-in the application stopped asking who was there.

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 payments application: iOS, Android, a supporting web interface, and the services behind them. Users authenticate, hold a balance and move money to other accounts. The assessment covered the applications and the transaction flows.

What follows is a set of findings that individually read as medium and collectively mean something worse, which is the usual shape of a payments assessment. Read one at a time they are gaps. Read together they say that everything after the sign-in screen was trusting the session and asking nothing further.

What we found at the moment money moved

The central finding is a single sentence: a transfer completed with no second factor, no re-authentication, and no confirmation step tied to the transaction itself. Sign-in was the last time the application asked the user to prove anything.

That means anyone holding a live session could move money. An unlocked phone. A device someone else set down. A session token recovered from anywhere it had been stored. The application had no way to tell those cases apart from the account holder, because after sign-in it stopped asking.

The distinction here is the one thing worth carrying away from this engagement, and it is covered at length in the guide on step-up authentication. Authentication establishes identity. It says nothing about intent, and intent is the only question a payment raises.

What was underneath that

A short numeric PIN, and no limit on trying it

The local barrier was a numeric PIN, short enough that the entire space of possible values could be worked through rather than guessed at. On its own that is a weak control. Combined with the next finding it stopped being a control at all.

The transaction endpoint had no rate limit. Nothing counted attempts, nothing slowed after failures, nothing raised an alert. Anything that could be tried once could be tried continuously, which is what turns a short secret from a weakness into an open door.

A transfer that should not have been accepted

The application accepted a transfer that made no sense as a transaction and that its own interface would never have produced. That is not an exploit. It is evidence, and the thing it is evidence of is that the transfer was being validated by the screen rather than by the server.

This is how business logic findings usually present: not as a dramatic result, but as the application agreeing to something that should have been refused. The severity comes from what else is true once you know the server is not checking.

Responses that carried more than the screen showed

Account attributes the interface never rendered were present in the raw responses, available to anyone reading the traffic rather than the screen. This is the ordinary shape of excessive data exposure: the API returns the object, the client shows part of it, and everybody assumes the part shown is the part sent.

And the defence that was meant to prevent all of this

Root and jailbreak detection was absent on one build and bypassable on the hardened one. That matters here more than it usually does, because every finding above is reached by reading the application’s own traffic, and this was the control that was supposed to make that difficult.

Why no scanner reports any of it

It is worth being precise about this, because "automated testing found nothing" is a sentence a lot of clients have been told and reasonably believed.

What each finding required to be found at all
FindingWhat finding it depended on
No second factor at transferKnowing that a transfer is a step where one is expected. There is no defect to detect; the application is doing what it was built to do.
Short PIN with no attempt limitA real account, a real transfer flow, and repeating a legitimate action to see whether anything intervened.
Transfer accepted that should not have beenUnderstanding what the transaction means in business terms, and then constructing one the interface cannot produce.
More returned than displayedComparing the raw response against the screen, which requires knowing what the screen is meant to show.
Device protections bypassableA device, tooling, and the time to work through it.

Every one of them came from a person with an account, working the flow the way a user does and then the way a user does not. That is the whole difference, and it is the reason a payments application is a poor candidate for scanning alone.

The fix

  1. Step-up authentication bound to the transaction rather than the session, with the amount and destination part of what the user approves - so approving one transfer cannot be replayed to authorise a different one.
  2. Rate limits on the transaction path, on the PIN, and on anything else that can be attempted repeatedly, with the limit keyed to the caller rather than the target.
  3. Server-side validation of the transfer itself, so that a request the interface could not have produced is refused on its merits rather than accepted because it was well formed.
  4. Responses reduced to the fields the screen needs, at the point the response is built rather than at the point it is displayed.
  5. Device integrity checks retained, and treated as signals rather than as the reason a server-side control can be omitted.

The findings, in one place

  • A transfer completed with no second factor, no re-authentication and no confirmation bound to the transaction itself.
  • The transaction endpoint had no rate limit, so anything that could be tried once could be tried continuously.
  • A short numeric PIN was the only local barrier, and it was short enough to be exhausted rather than guessed.
  • The application accepted a transfer that made no sense as a transaction, which is what an absent server-side check looks like from the outside.
  • Not one of these findings is something a scanner reports, because none of them is a defect in a component. They are decisions the application makes.

What to check on your own system

If you build anything that moves money, changes an entitlement, or alters who can reach an account, the interesting question is never whether someone can sign in. It is what stands between a live session and the action. In this application the answer was nothing, and no individual finding said so. It only became visible once they were read together, which is the argument for having somebody assess the flow rather than the components.

The action worth checking first

In our reports the single most frequently unprotected sensitive action is not the payment. It is the change of recovery email or phone number, because that one converts a temporary hold on a session into permanent control of the account. If you are auditing your own step-up rules this week, start there.

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

We use multi-factor authentication at login. Is that not enough?

It protects the login. It does nothing for the session that follows, which is where the transaction happens. Multi-factor authentication at sign-in and no step-up at the payment is the exact configuration this engagement describes, and it is extremely common precisely because it feels like the box is ticked.

Are business logic flaws really that common?

In applications that do something specific with money, entitlements or workflow, they are the findings that carry the most risk, and they are almost entirely absent from automated results. They are also the ones that cannot be found without somebody being told what the application is for, which is why the scoping call spends time on business process rather than technology.

How long does an assessment like this take?

Our engagements run five to ten business days depending on scope, with the price fixed once the scope is agreed. A payments application with mobile clients sits at the longer end because the transaction flows have to be worked through by hand, and because getting to the traffic on a hardened mobile build is itself part of the work.

Would you have found this from the web interface alone?

Some of it. The absent step-up and the unvalidated transfer would have shown up. The device protections and the local PIN handling would not have been in scope at all, and those were the findings that made the others reachable in practice rather than in theory.

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.