Skip to content
Case study

The Breach That Looks Like a Bug: How Broken Authorisation Hides in a Support Queue

One identifier, changed, cancelled a payment somebody else had scheduled. Another deleted the card they had saved. Neither leaves the victim any sign of an attack - both look exactly like the platform malfunctioning, which is why the half of an authorisation flaw nobody tests is also the half that costs the most.

6 min read
The system
A web application where people store a payment method and schedule payments against an account, and both parties can see the state of it.
In scope
The web application and its supporting endpoints.
What was at risk
Other people’s scheduled payments and stored payment methods, cancellable and deletable by anybody with an account - and experienced by the victim as a failure of the product rather than as an attack.

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 web application where an account holder saves a payment method, schedules payments against it, and can see the state of each one. Records are set up between two parties and both of them can see whether the steps that establish a record have been completed. The web application and its endpoints were in scope.

What we found

Cancelling somebody else’s scheduled payment

The endpoint took an identifier for the scheduled payment and acted on it. It confirmed that the caller was authenticated. It did not confirm the payment belonged to them. Substituting an identifier cancelled a payment scheduled by somebody else.

Deleting somebody else’s saved payment method

The same shape, with a permanent result. The stored payment method of another user could be removed. Not read, not copied - removed, so the next charge against it fails.

Reading somebody else’s record

The read-side version was present too: the state of another party’s record, and whether the approvals that establish it had been completed, retrievable by identifier. On most engagements this is the headline finding. Here it was the least of the three.

And what was left in the responses

Responses carried developer commentary left in from build time, including detail about the layer underneath the application. That is not exploitable by itself. It is a map, and it removes most of the guesswork for whoever arrives next. Alongside it, the session cookie was set without the flags that stop it being read by script or sent over an unencrypted connection, and one field was rendered back to other users without being encoded, which is the ordinary route to cross-site scripting.

Why the write side costs more

Read-side authorisation flaws are the ones that get found, reported and prioritised. The impact sentence writes itself: somebody saw something they should not have. Everybody in the room understands it.

The write side is harder to describe and worse to experience. A cancelled payment is one that simply does not happen, discovered when the consequence arrives rather than when the cancellation does. A deleted payment method is a charge that fails for no reason anyone can see. Neither leaves the victim with any evidence of an attack. Both look exactly like the platform being broken.

The same flaw, read and write
Read sideWrite side
What the attacker getsInformation belonging to someone elseControl of something belonging to someone else
What the victim seesNothing, until toldA failure they attribute to the platform
What the platform seesA legitimate-looking requestA legitimate-looking request
How it is usually discoveredA report, a disclosure, or an assessmentA support ticket about a bug
Cost to the businessDisclosure, regulatory exposure, trustAll of that, plus the transaction that did not happen and the work to put the data back

That last row is the reason this engagement is worth writing up. The read-side finding is a breach. The write-side finding is a breach that first presents as a product quality problem, which means it is being handled by the wrong people for as long as it goes unrecognised.

The pattern behind all three

Every one of these findings has the same root: an identifier was trusted because it was assumed to be hard to guess.

Hard to guess is not an access control. It is also, usually, not true. Sequential identifiers enumerate. Long random ones leak through shared links, exported reports, support attachments, browser history and referrer headers. And the moment one identifier is known, an endpoint that checks nothing else will act on it.

The specific mistake in code is almost always the same: the ownership check exists on the page that lists the records, and not on the endpoint that acts on one. The list is where the developer was thinking about who the user was. The action was written afterwards, and it took the identifier at face value.

The fix

  1. Ownership resolved server-side on every action, from the authenticated session rather than from anything in the request. If the record is fetched by identifier and then checked, the check has to happen before anything is done with it.
  2. Destructive and financial actions reviewed first rather than last, on the basis that they are the ones where a missing check is unrecoverable.
  3. Debug and development output removed from production responses.
  4. Session cookie flags set, so the cookie cannot be read by script or sent in the clear.
  5. Output encoded where it is rendered rather than filtered where it is accepted, because the same value is safe in one context and dangerous in another.

The findings, in one place

  • Anyone could cancel another user’s scheduled payment, and anyone could delete another user’s saved payment method.
  • The state of other people’s records, including whether the required approvals were completed, was readable by identifier alone.
  • The pattern behind all of it: identifiers were trusted because they were assumed hard to guess, which is not a control and usually not true.
  • Write-side flaws are the expensive ones, because the victim experiences them as the platform malfunctioning rather than as an attack.
  • Developer commentary left in production responses gave a map of the layer underneath.

What to check on your own system

If you are reviewing your own authorisation this month, do not start with the pages that display data. List every endpoint that cancels, deletes, transfers, revokes, closes or reassigns something, and check each one resolves ownership from the session. That list is shorter than the read list, it is where the unrecoverable damage lives, and in our experience it is the half that was written in a hurry.

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

How do you test destructive actions without destroying real data?

Against accounts and records created for the assessment, in an environment agreed in the rules of engagement before testing starts. Each destructive finding is demonstrated once, against our own record, and documented so it can be reproduced by your developers without repeating it against anything live.

We use long random identifiers. Are we not covered?

They raise the effort of finding one identifier. They do nothing once one is known, and identifiers travel further than teams expect - into shared links, exports, support tickets and logs. Unguessable identifiers are a reasonable defence in depth measure and a poor access control, so use them, and check ownership anyway.

Would this have been caught in code review?

Sometimes, if the reviewer was looking for it specifically. The reason it survives is that each endpoint looks correct in isolation - it authenticates, it validates its input, it does what it says. The defect is a comparison that is absent, and absent code is the hardest thing to notice in a diff.

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.