Estimated Savings: > $50,000 in immediate, direct fraud losses averted
Executive Summary
Cyvocate identified, verified, and reported a critical financial logic vulnerability in a fast-growing fintech application. The platform—built with a Node.js/Express API gateway, a PostgreSQL database, and integrated with leading Internet Payment Gateways (IPGs) like Stripe and Adyen—suffered from an amount manipulation flow in its user wallet recharge pipeline.
A severe oversight in the transaction reconciliation logic allowed malicious users to modify POST parameters during payment initialization, charging their credit card $1 while successfully crediting their digital wallet with $100. If exploited at scale, this vulnerability would have resulted in catastrophic direct revenue losses, balance sheet inflation, and compliance penalties. Cyvocate responsibly disclosed the vulnerability and refactored the transaction pipeline to enforce absolute mathematical consistency.
The Tech Stack
- Backend Environment: Node.js (v20+), Express.js
- Database: PostgreSQL (ACID compliant storage engine)
- Payment Providers: Stripe API, Adyen integration
- Proxy Testing Tools: Burp Suite Professional
The Challenge
Fintech applications are highly target-rich environments where minor design flaws can lead to direct theft of assets. The application under review offered a custom balance system, letting users load fiat currency onto their profiles before executing trades or purchasing goods.
The core challenge was a lack of unified state synchronization between the payment initialization state, the third-party IPG checkout session, and the wallet reconciliation handler. The frontend client initiated a checkout, and the backend generated payment details. However, the system trusted parameter inputs in the webhook or final receipt callback without checking the session metadata on the IPG's servers.
This parameter asymmetry opened the door for simple, high-impact parameter tampering.
The Exploit Scenario
Using Burp Suite to intercept traffic between the browser, backend server, and the payment gateway, Cyvocate’s engineers analyzed the recharge lifecycle:
- Recharge Initialization: The user selects a recharge value (e.g., $100) and triggers the checkout. The client posts payment parameters to the backend.
- Intercepting Parameters: The attacker intercepts the outbound POST request to
/api/recharge/createwith a proxy tool. - Parameter Discrepancy Injection: The attacker notices two parallel fields:
payment_amount-> The numerical value transmitted to the IPG processing engine ($1).recharge_amount-> The credit value assigned to the user’s wallet on success ($100).
- Gateway Execution: The payment gateway successfully charges the card $1 and returns a success status.
- Direct Balance Invalidation: The Express backend receives the success response from the gateway. Because the backend code failed to verify if
payment_amountandrecharge_amountmatched, it credited the attacker’s database record with the full $100.
At scale, an attacker could invest $500 in real payments and receive $50,000 in verified wallet credit.
Proof of Concept
1. Intercepting the Parameters via Proxy
We analyzed the API request parameters using Burp Suite and isolated the logic flaw:

The application accepted separate parameters for the settlement charge and the wallet ledger increment.
2. Crafting the Tampered Request
We sent a test transaction through our proxy, modifying the balance parameters to charge only $1 while requesting a $100 credit.

The payment gateway executed the $1 charge. When the webhook resolved, the client’s backend verified that the $1 transaction completed successfully, but then wrote $100 to the PostgreSQL ledger.
Code Remedy: Insecure vs. Secure Webhooks & Transactions
To help fintech developers prevent payment tampering, we provide the comparison below showing the original vulnerable Express controller and the secure, signature-verified transactional controller designed by Cyvocate.
❌ The Insecure Implementation (Node.js/Express)
In this implementation, the Express controller trusted parameters submitted within the API body and failed to reconcile the values against the payment gateway's actual settlement receipt:
// INSECURE: Controller trusts client parameters and updates ledger without reconciliation
app.post('/api/recharge/callback', async (req, res) => {
const { payment_id, userId, payment_amount, recharge_amount, status } = req.body;
// CRITICAL: trusts parameter variables instead of checking gateway state
if (status === 'success') {
// Vuln: We pay $1 (payment_amount) but receive $100 (recharge_amount)
await db.query(
'UPDATE users SET wallet_balance = wallet_balance + $1 WHERE id = $2',
[recharge_amount, userId]
);
return res.status(200).json({ success: true });
}
return res.status(400).json({ success: false });
});
The Secure Implementation (Node.js + PostgreSQL ACID Transactions)
To secure the application, Cyvocate redesigned the workflow. The backend now initiates a secure checkout session with the IPG (e.g. Stripe), attaches metadata, and utilizes cryptographic webhook signature verification to process callbacks. The balance update is strictly handled using PostgreSQL isolation levels and transactional rollbacks to enforce absolute consistency:
import express from 'express';
import Stripe from 'stripe';
import pg from 'pg';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' });
const pool = new pg.Pool();
app.post('/api/recharge/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature']!;
let event: Stripe.Event;
// 1. Authenticate that the event was actually sent by the payment provider
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
console.error(`Webhook Signature Verification Failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// 2. Process successfully completed checkout sessions
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
// Extract metadata injected securely during payment initialization
const userId = session.metadata?.userId;
const paymentAmountCents = session.amount_total; // Securely fetched from Stripe's ledger
if (!userId || !paymentAmountCents) {
return res.status(400).send("Invalid transaction metadata.");
}
const dbClient = await pool.connect();
try {
// 3. Initiate highly-isolated database transaction (Serializable/Read Committed)
await dbClient.query('BEGIN');
// 4. Calculate actual credit amount directly from Stripe's verified payment amount
const paymentAmountDollars = paymentAmountCents / 100;
// 5. Update wallet balance using parameters controlled strictly by the backend
const updateQuery = `
UPDATE users
SET wallet_balance = wallet_balance + $1
WHERE id = $2
RETURNING wallet_balance;
`;
const result = await dbClient.query(updateQuery, [paymentAmountDollars, userId]);
// 6. Log transaction event securely for audit logging
await dbClient.query(
'INSERT INTO wallet_transactions (user_id, stripe_session_id, amount, status) VALUES ($1, $2, $3, $4)',
[userId, session.id, paymentAmountDollars, 'completed']
);
// Commit the transaction to enforce ACID consistency
await dbClient.query('COMMIT');
res.status(200).json({ received: true });
} catch (dbError) {
// Rollback database modifications if any step fails to avoid orphan records
await dbClient.query('ROLLBACK');
console.error("Database Transaction Failed, Rolled Back: ", dbError);
res.status(500).send("Internal Ledger Error");
} finally {
dbClient.release();
}
} else {
res.status(200).json({ received: true });
}
});
Recommendations & Mitigation Controls
- Enforce Cryptographic Webhooks: Never trust HTTP GET or POST callbacks that can be triggered directly by the client browser or modified in a proxy. Always enforce cryptographic webhook signatures (e.g., Stripe Signatures, Adyen SHA256 signatures).
- Verify Amount Consistency Server-Side: Validate that the amount calculated during order creation matches the amount processed by the gateway exactly.
- Strict Transaction Isolation: Use SQL transactions (
BEGIN ... COMMIT) to ensure that credit assignments are atomic and consistently locked during execution to prevent race conditions. - Continuous Ledger Reconciliation: Implement automated backend cron jobs to execute ledger checksum audits, matching raw gateway invoices against database updates.