July 23, 2026
The Transaction Is the Attack Surface
Threat modeling when the asset you’re protecting is money.

By Andrews Ferreira
5 min read
Most threat models in payments start in the wrong place. Someone opens the architecture diagram, draws trust boundaries around the boxes, walks STRIDE across each component, and produces a tidy list of spoofing and tampering risks per service. It looks like rigor. It usually misses the point.
The attacker in a payment environment rarely wants to compromise your service for its own sake. They want to move value they shouldn't be able to move, or read data they shouldn't be able to read. Those are different goals from owning the host, and a component-by-component model rarely surfaces them. You can have every box hardened, every port closed, every dependency patched — and still lose money through a flow that behaves exactly as designed.
So the first thing I've learned to do is stop modeling components and start modeling the transaction.
The diagram lies; the state machine tells the truth
A payment isn't a request. It's a lifecycle: authorization, capture, settlement, refund, authorization reversal or void, and, in some cases, chargeback. The exact states and transitions vary by processor and payment rail, but the security question stays the same: what value can move at each transition, who can move it, and under which conditions? Money — or the promise of money — changes state at each step, and every transition is a place where an attacker, or an honest system under stress, can do something the designer didn't intend.
The architecture diagram shows you where the code runs. The state machine shows you where the value moves. When I model a payment feature, I care far less about "which service talks to which" and far more about: what are the valid states and transitions of this transaction, what triggers each transition, who is allowed to trigger it, and what happens if a transition fires when it shouldn't — twice, out of order, or after the window that was supposed to close it.
That reframing changes the questions. Instead of "can this endpoint be spoofed," you ask "can a capture be issued against an authorization that was already voided?" Instead of "is this input validated," you ask "can cumulative refunds exceed the amount that was actually captured?" Those are the findings that cost real money, and they almost never show up when you model boxes. Each of them is the same question in disguise: which invariant is supposed to hold, and what breaks it?
What happens if this runs twice
Idempotency sounds like an engineering nicety — a way to keep retries from creating duplicate records. In payments it's a security control, and treating it as anything less is how you end up paying the same payout twice.
Networks fail. Clients retry. Queues redeliver. In many systems, a duplicated request is a recoverable operational bug. In a system that moves money, a duplicated request can become a double capture, a double credit, a double refund, or a double spend — and an attacker who understands this will deliberately induce the retry, replay the operation, race concurrent requests, and keep whichever outcome favors them. So one of the questions I bring to every payment threat model is blunt: what happens if this exact operation executes twice, three times, concurrently? If the answer depends on a database uniqueness constraint that "should" be there, that's not an answer by itself.
The same logic extends to ordering and timing. Two requests hitting the same balance at the same instant. A refund requested before the transaction reaches a refundable state. A limit check that reads a value, thinks, and then acts on a number that changed while it was thinking. These time-of-check-to-time-of-use gaps are theoretical curiosities in a lot of software. When the value under the race is a balance, they are a withdrawal strategy.
The call that comes from inside
There's a comfortable assumption that lives in a lot of internal architectures: traffic from another one of our services is trusted traffic. The perimeter model died years ago and most people know it, but the instinct survives in the way internal calls get modeled — or don't get modeled at all.
In a payment environment I model internal service-to-service calls as if the neighbor is already compromised. Not because I distrust the team that built it, but because the blast radius of assuming otherwise is unacceptable. If the service that authorizes a transaction believes anything the upstream service tells it without its own checks, then compromising the weakest, least-scrutinized internal service becomes a path to authorizing whatever you like. The interesting trust boundary is almost never the one at the edge. It's the quiet one between two internal services that were built by different teams, at different times, under different assumptions about who was responsible for validation.
PCI is an outcome of design, not a checklist you pass
The weakest threat modeling I see around cardholder data treats compliance as a scope to be defended: here is where the card data lives, wrap controls around it, prove it to the assessor. That works, but it's expensive, and the scope is often larger than it needs to be.
The stronger move happens earlier, in design. Every system that touches a primary account number is a system you now have to protect, audit, and justify. So the most valuable threat modeling question about card data is often just this: does this component need to see it at all? Tokenization, done as a design decision rather than a compliance retrofit, doesn't just satisfy a requirement — it shrinks the set of systems in scope, which shrinks the set of systems that can leak. The best control is the data that never arrives. I'd rather spend an architecture review arguing a service out of the cardholder data environment entirely than spend a year proving the controls around it are adequate.
Compliance frameworks describe the floor. Threat modeling is how you engineer the risk out instead of fencing it in — so the floor is easy to stand on.
The legitimate user who is lying
Many application security exercises still default to an external attacker: someone without credentials trying to get in. Payments forces a harder character into the model — the authenticated user who chains individually permitted actions into an outcome the system should never allow.
This is where the line between "vulnerability" and "fraud" dissolves, and where a lot of security programs fall down because they've decided one of those words is someone else's problem. Business-logic abuse — chaining legitimate operations into an illegitimate outcome, exploiting a promotional flow, walking a sequence of valid state transitions to a place the designer never imagined — doesn't look like an attack in any single request. Every step is authorized. The exploit is the composition. You will not find it by scanning inputs; you find it by sitting with the flow and asking what a motivated, patient, fully-authenticated adversary could assemble out of the moves you've handed them.
Where this fits, and when
None of this is an audit you run after the code ships. Threat modeling for payments earns its value as a design conversation — before the architecture is set, when changing a trust assumption still costs a discussion instead of a rewrite. I bring it to new products, to new flows, and to any change that touches how value moves or how card data travels. The output isn't a document that proves diligence. It's a shared understanding, held by the engineers who will build the thing, of where the money could go wrong — so they design against it without me in the room.
Because that's the real goal. In a payment system, threat modeling isn't about enumerating vulnerabilities. It's about understanding where value flows, and where someone could bend that flow toward themselves. Model the boxes and you'll harden the infrastructure. Model the transaction and you'll protect the money.
The next time someone opens an architecture diagram for a payment feature, don't ask which boxes are exposed first. Ask which state transition moves value, what invariant protects it, and what happens when that invariant fails. That's where payment threat models stop becoming documentation and start becoming engineering.
Follow the value. The attack surface is wherever it moves.