← back to research

Minting wallet money from the open internet

A consumer fintech asked Cipher to extend a review into its infrastructure. Modeling the wallet's request flow end to end surfaced an internal money-crediting endpoint reachable, unauthenticated, from the open internet (proven with a reverted credit).


A consumer fintech app (an in-app wallet with a rewards program) brought Cipher in to review the application, then extended the scope into the infrastructure layer. The finding that mattered most did not come from either layer alone. It came from reading them together: an internal endpoint that credits wallet balance turned out to be reachable, unauthenticated, by an anonymous client on the open internet. This is how Cipher arrived at it, and why it stayed invisible to the controls already in place.

First, learn the request flow

To reason about exposure, you have to know how a request actually travels. Cipher built that model from ground truth (the gateway’s infrastructure-as-code and the application’s own authorization code) rather than from a diagram:

public gateway domain → per-route configuration → the gateway’s auth plugin and the set of routes it actually protects → internal services behind an internal load balancer → the wallet ledger → an event bus that propagates balance changes into the spendable wallet.

The last hop is the one that makes everything upstream matter. Anything that writes the ledger does not stay in the service that wrote it; it becomes usable, withdrawable balance downstream.

Two facts, side by side

Reading the gateway configuration end to end, two facts sat next to each other. An internal reward-crediting endpoint, a POST to an /admin/.../credit path, was published on the public gateway domain. And the gateway’s auth plugin listed only the rewards service’s ordinary read and write routes among the ones it protected; the admin-credit route was absent from that list.

A route that is published publicly but missing from every auth plugin’s protected set is forwarded to the backend with no authentication at all. The only control left standing was an application-level authorizer on the handler. And that handler credited a caller-chosen user a caller-chosen amount, keyed by a server-generated random identifier, while the authorizer compared a request header against a single static token with no rate limiting. The hypothesis wrote itself: an anonymous client on the internet, holding only that weak token, could create wallet balance from nothing.

Proving it without guesswork

Confirmation came from the system, not from a wordlist. A non-destructive probe to the public gateway with an empty body came back with the application’s own “admin token invalid” message, not the gateway’s own rejection. That single detail proved the gateway had forwarded the request all the way to the backend, and the same diagnostic revealed the deployed token was a low-entropy numeric value.

With that, an end-to-end proof run from off the corporate network credited one unit of cash to a test wallet, then reverted it through the internal revert path: balance back to zero, nothing left behind. Two properties made the impact serious rather than theoretical. The crediting call generated its own random reference id, so the ledger’s duplicate-protection never engaged: the call was unboundedly repeatable, with no cap on the amount. And the balance it created propagated over the event bus into the spendable wallet. A sanctioned no-token probe against the production edge returned the same forwarding behavior, which moved the blast radius from a staging environment to the production perimeter.

For a fintech wallet, that is a direct financial-loss path: an anonymous party on the open internet creating spendable money, repeatably, against any account.

Why the existing controls never saw it

The platform was not unprotected. There was an auth plugin on the gateway. There was an authorizer on the handler. A scanner pointed at either layer would have found nothing to flag: the route configuration was valid, and the application code did exactly what it was written to do. The exposure lived in the relationship between the two: an infrastructure route list that didn’t include one route, and an application that assumed the gateway in front of it had already turned anonymous callers away. Its severity was only legible to someone who also knew that a write to the ledger becomes spendable balance two hops later.

Seeing it meant holding the whole request flow in view at once: gateway, route configuration, auth plugin, internal services, ledger, event bus. That cross-layer reasoning is the work, and it is the part a single-layer tool cannot do.

Proven, then reverted

The finding was validated against the live system with a non-destructive proof that was reverted immediately, and handed back with the fix: keep internal crediting endpoints off the public gateway, and fail the build when an admin route ships without an auth plugin attached; replace the shared static token with per-caller credentials, compared in constant time and rotated; make crediting idempotent on a caller-supplied id, with amount and per-user caps. The controls were there. What was missing was one invariant, enforced across the infrastructure and the application at the same time: an internal money endpoint is never reachable unauthenticated, from anywhere. Holding both layers in view is what made it visible.