Why lending APIs have a unique security profile

Lending is different from payments. A payment moves existing funds from A to B. A loan disbursement creates new funds — the lender extends credit and transfers money to the borrower. The authorization question for a payment is: does this account have enough balance? The authorization question for a loan disbursement is: has this loan been approved, and has it not been disbursed before? Both conditions must be true and must be checked atomically. If the second check can be bypassed through concurrent requests, replay attacks, or status manipulation, the lender disburses money without a valid approval.

The fraud loss from a disbursement logic flaw is also different from a payment fraud loss. In a payment fraud, the lender helps move existing funds. In a disbursement fraud, the lender creates a new liability — money goes out but no corresponding asset (a collectible loan) was created against it in the accounting system. Recovery requires civil action against the borrower, not a bank reversal.

1. Concurrent disbursement request race condition

The disbursement logic on most lending APIs follows this pattern: check if the loan is in "approved" status, mark it as "disbursing", send the transfer instruction to the payment partner, mark it as "disbursed". Between the approval check and the status update, there is a window where a second concurrent request can also pass the approval check and initiate a second disbursement. We test this by firing multiple simultaneous disbursement requests for the same loan ID and observing whether more than one disbursement is executed.

// VULNERABLE: check and update are not atomic
async function disburseLoan(loanId) {
  const loan = await db.query(
    "SELECT * FROM loans WHERE id = $1 AND status = 'approved'", [loanId]
  );
  if (!loan) throw new Error("Loan not eligible");

  // RACE WINDOW: two concurrent calls can both reach here
  await db.query(
    "UPDATE loans SET status = 'disbursing' WHERE id = $1", [loanId]
  );
  await paymentPartner.transfer(loan.borrowerAccount, loan.amount);
  await db.query(
    "UPDATE loans SET status = 'disbursed' WHERE id = $1", [loanId]
  );
}

2. Loan amount manipulation between approval and disbursement

When a loan is approved, the approval record stores the approved amount. The disbursement API should read this amount from the approval record — not from a parameter in the disbursement request. We test whether the disbursement API accepts an amount parameter and uses it instead of the database-stored approval amount. If it does, an attacker who triggers their own disbursement can set the amount to any value, receiving more than was approved.

3. Disbursement webhook replay for double credit

After disbursement, the payment partner sends a webhook notification to the lender confirming the transfer. The lender's webhook handler marks the loan as fully disbursed and updates the borrower's loan record. We test whether this webhook handler is idempotent — processing the same webhook reference twice must produce the same result (one disbursement marked complete) not two credits to the borrower's account. A non-idempotent handler that credits the borrower for each webhook delivery is exploitable through replay.

4. Debt cap bypass through application timing

Most lending platforms enforce a maximum outstanding debt per borrower — a cap on how much one person can owe at a time. We test whether this cap is checked at application time, at approval time, or at disbursement time. If the check only happens at application time, a borrower can have multiple applications approved simultaneously before any disbursement reduces their remaining headroom. By the time all approved loans are disbursed, the borrower has received multiples of the intended maximum exposure.

Real finding from a digital lending engagement

Concurrent disbursement race condition resulting in double transfer

During a penetration test of a Nigerian digital lender processing personal loans between 50,000 and 500,000 naira, we identified that the disbursement function was not protected by an atomic database lock. We submitted 10 concurrent POST requests to the disbursement endpoint for the same approved loan ID using a parallel request tool. Two of the requests passed the approved status check simultaneously before either had updated the status to "disbursing". Both requests initiated transfers through the payment partner. The borrower received two transfers totaling twice the loan amount. The lender's system showed one loan as "disbursed" but two transfer records were created. Fix priority: critical. Remediated by wrapping the status check and update in a PostgreSQL transaction with SELECT FOR UPDATE, preventing any other transaction from reading the loan record until the status update is committed.

Operating a digital lending platform in Nigeria? Book a security assessment that covers your full disbursement pipeline and loan lifecycle logic.

Book a Lending Platform Pentest

Frequently asked questions

What is the most common business logic flaw in Nigerian digital lending APIs?

The most common flaw is the absence of an atomic lock between loan approval and disbursement. When these two operations are separate database queries without a transaction-level lock, concurrent requests can both pass the approval check and both trigger disbursement — effectively disbursing twice for one approved loan. We find this pattern in roughly one-third of the Nigerian lending platforms we assess.

Can loan amount manipulation happen after approval?

Yes. If the disbursement API accepts the loan amount as a client parameter rather than reading it from the server-side approval record, an attacker can modify the disbursement amount upward between approval and transfer execution. The fix is for the disbursement function to read the amount exclusively from the approval record in the database, ignoring any client-submitted amount.

What regulatory obligations apply to digital lenders in Nigeria regarding fraud?

CBN-licensed lenders must report fraud incidents to the Nigerian Financial Intelligence Unit (NFIU) under the Money Laundering (Prevention and Prohibition) Act. CBN's consumer protection regulations also require that digital lenders have documented fraud prevention controls. Evidence of exploitable disbursement API logic flaws during a regulatory examination signals inadequate fraud controls.

Related reading

Blog: BNPL platform security · Webhook race conditions · NIBSS reconciliation race conditions

Services: Penetration testing · API security