Skip to content
All insights

Security & Quality

Beyond SAST: what a manual source-code audit finds in a fintech codebase

5 min readWarmbytes Engineering

Every fintech engineering team we've worked with already runs a SAST tool — Semgrep, SonarQube, a language-specific linter with a security ruleset, or a commercial scanner wired into CI. That's correct: static analysis is cheap per run, catches an entire class of defects reliably, and belongs in the pipeline regardless of what else happens. But teams that treat a clean SAST report as evidence the codebase is safe are measuring the wrong thing. SAST is good at finding patterns. It is structurally unable to find defects that only exist relative to what the system is supposed to do with money.

What static analysis actually checks

A static analyzer walks an AST or a control-flow graph looking for shapes it already knows are dangerous: string concatenation flowing into a SQL execution call, user input reaching a deserializer, a hardcoded credential literal, a try block that swallows an exception without logging it. These are genuinely valuable findings — they are also, definitionally, findings the tool's authors anticipated in advance. A SAST rule set encodes "here is a pattern we've seen be dangerous before." It cannot encode "here is a pattern specific to this system's business logic that would only be dangerous because of what this endpoint means."

The class of bug static analysis cannot see

The defects that actually cost fintech platforms money tend to live in the gap between what the code does and what the code is supposed to guarantee — a gap no tool can see without understanding the guarantee. A few patterns that come up repeatedly in manual review and essentially never in a scanner report:

  • State-machine gaps. A transaction has states — initiated, pending, settled, reversed — and a specific set of legal transitions between them. The code enforces most of those transitions correctly and misses one: a reversed transaction that can still be re-submitted through a retry path that predates the reversal state being added. Nothing here looks unusual to a scanner; every individual line is syntactically fine.
  • Authorization that checks the wrong scope. An endpoint correctly verifies that the caller is authenticated, and correctly verifies that the resource ID in the request exists — but never checks that the resource belongs to the authenticated caller's account. This is a business-logic IDOR, and it reads, line by line, exactly like correct code.
  • Idempotency keys that aren't actually idempotent. A payment endpoint accepts an idempotency key and looks it up before processing — but the lookup and the insert aren't in the same transaction, so two concurrent requests with the same key both pass the "does this key already exist" check before either has written its row. The fix is a unique constraint plus a conflict handler, not a code review comment; finding it requires reading the concurrency behaviour, not the syntax.
  • Silent precision loss. Money handled as a floating-point type somewhere in a pipeline that otherwise uses fixed-point decimals or integer minor units. A scanner has no concept of "this value represents currency and this arithmetic is lossy for currency" — that's domain knowledge, not a pattern.
  • Reconciliation blind spots. A webhook handler updates internal state on receipt of a provider callback, with no corresponding scheduled reconciliation job to catch the case where the callback never arrives. The code that exists is correct; the absence of code that should also exist is the actual finding.

None of these show up as a rule violation, because none of them are a pattern in the code — they're a mismatch between the code and a domain invariant the code is supposed to hold.

What a manual review actually does differently

A useful source-code audit starts from the business logic, not the syntax tree: what is this endpoint supposed to guarantee, and does the code, read end to end, actually guarantee it? That means tracing a transaction's full lifecycle across the files that touch it rather than reviewing files in isolation, deliberately asking "what happens if this arrives twice," "what happens if this arrives out of order," and "what happens if this fails halfway," and treating authorization as a per-endpoint question about data scope, not a single shared middleware check assumed to cover everything downstream of it.

This is also where white-box testing earns its cost over black-box penetration testing alone: with the source in front of you, you can follow a suspicious code path to its actual boundary conditions instead of inferring behaviour from the outside through trial and error.

Where this fits, not what it replaces

None of this is an argument against SAST — it's an argument for running both, because they find different things for different reasons. A mature review process runs static analysis continuously in CI to catch the mechanical class of defect on every commit, and runs a manual, business-logic-aware audit periodically — before a major release, after a period of rapid feature growth, or as an outside check a team cannot easily run on its own code — to catch the class of defect that only exists relative to intent.

One caveat worth stating plainly: a clean audit report, static or manual, is a statement about what was found in the time available against the access granted. It is not a certification, and shouldn't be represented as one. The useful output of an audit is a prioritized, actionable list of what to fix and why it matters — not a badge.

securitycode-auditfintechstatic-analysis