Samen Steeve
MY SERVICES.
Back to blog
SecurityFintechAudit
December 10, 20257 min read

Fintech Security Audit: Insights from 3 Days of Offensive Testing

In the fast-moving Central African fintech space, rushing features to market can sometimes leave security sidelined. Recently, I conducted a 3-day offensive security audit of a local money transfer and payment platform. Here are the three real-world vulnerabilities uncovered during the pentest and how we resolved them.

1. BOLA (Broken Object-Level Authorization) on Digital Receipts

BOLA (formerly known as IDOR) ranks at the top of the OWASP API Security Top 10. In this application, after a user completed a fund transfer, a receipt could be downloaded via the following endpoint:

GET https://api.fintech-locale.cm/v1/receipts/84209

The Vulnerability: An attacker could simply increment the numerical ID in the URL (e.g. changing it to 84210) to access receipts belonging to other users. The API verified that the caller's JWT token was valid, but it failed to assert that the authenticated user was the actual owner or recipient of the requested receipt.

The Fix: Swapped sequential IDs for non-predictable UUID v4 strings, and implemented authorization checks at the controller level (Laravel Policy):

// Laravel Policy: Asserting strict object ownership
public function view(User $user, Receipt $receipt)
{
    // User must be the sender or the recipient of the matching transaction
    return $user->id === $receipt->sender_id || $user->id === $receipt->recipient_id;
}

2. OTP Verification Brute-Forcing (No Rate Limiting)

To authorize a cash withdrawal, the API generated a 4-digit SMS OTP. The verification request was structured as follows:

POST https://api.fintech-locale.cm/v1/otp/verify { "transaction_id": 451, "code": "1234" }

The Vulnerability: The verification endpoint did not enforce any rate limiting. An attacker could cycle through all 10,000 combinations (from 0000 to 9999) in less than 30 seconds using parallelized HTTP client requests, successfully authenticating a fraudulent withdrawal.

The Fix: Capped OTP verification attempts to 3 per transaction, set an OTP TTL expiration to 2 minutes, and set up a rate limiter middleware bound to both the client's IP and user accounts.

3. SQL Injection in Dynamic Reporting Queries

The merchant dashboard allowed users to sort transaction history. The underlying database query concatenated the user-supplied column name directly into the SQL string:

// DETECTED VULNERABLE CODE
$sortBy = $request->input('sort_by'); // E.g. "created_at"
$results = DB::select("SELECT * FROM transactions WHERE merchant_id = $id ORDER BY " . $sortBy);

The Vulnerability: Attackers injected arbitrary SQL statements into the sort_by payload (e.g. created_at; DROP TABLE users; or time-based blind SQL injections to extract user hashes).

The Fix: Validated variables against an allowed whitelist of fields before executing queries:

// Whitelist-based validation
$allowedSorts = ['created_at', 'amount', 'status'];
$sortBy = in_array($request->input('sort_by'), $allowedSorts) ? $request->input('sort_by') : 'created_at';

$results = DB::table('transactions')
    ->where('merchant_id', $merchantId)
    ->orderBy($sortBy)
    ->get();

The Takeaway

These security issues are not complex cryptographic flaws; they are common application logic errors. Regular codebase audits and adhering to OWASP rules (systematic object-level authorization, rate limiting on authentication flows, and parameterized SQL queries) are critical to maintaining user trust and transaction safety.

Need an offensive security audit?

Let's uncover your logical bypasses, BOLA, and SSRF vulnerabilities before they are exploited.

Start a project