Double-Spending Through Concurrent Transaction Processing
Financial systems that process transactions without proper locking mechanisms allow attackers to initiate multiple simultaneous transactions that reference the same account balance, potentially spending funds multiple times before the balance is updated.
Preview example – JAVASCRIPT
// VULNERABLE: Concurrent transaction processing without locking
const express = require('express');
const app = express();
// PROBLEM: Transaction processing without atomic operations
app.post('/api/transfer', async (req, res) => {
const { fromAccountId, toAccountId, amount } = req.body;
try {
// VULNERABLE: Non-atomic balance checks and updates
const fromAccount = await Account.findById(fromAccountId);
const toAccount = await Account.findById(toAccountId);
if (!fromAccount || !toAccount) {
return res.status(404).json({ error: 'Account not found' });
}
// PROBLEM 1: Check-then-act race condition
if (fromAccount.balance < amount) {
return res.status(400).json({ error: 'Insufficient funds' });
}
// RACE CONDITION: Balance can change between check and update
// Multiple concurrent requests can pass the balance check
// PROBLEM 2: Non-atomic balance updates
fromAccount.balance -= amount;
toAccount.balance += amount;
// VULNERABLE: Separate save operations
await fromAccount.save();
await toAccount.save();
// PROBLEM 3: Transaction record created after balance updates
const transaction = await Transaction.create({
fromAccountId,
toAccountId,
amount,
type: 'transfer',
status: 'completed',
timestamp: new Date()
});
res.json({
success: true,
transactionId: transaction.id,
newBalance: fromAccount.balance
});
} catch (error) {
res.status(500).json({ error: 'Transfer failed' });
}
});
// VULNERABLE: Withdrawal processing with race conditions
app.post('/api/withdraw', async (req, res) => {
const { accountId, amount } = req.body;
try {
const account = await Account.findById(accountId);
if (!account) {
return res.status(404).json({ error: 'Account not found' });
}
// PROBLEM: Daily limit check without atomic operations
const today = new Date().toDateString();
const todayWithdrawals = await Transaction.aggregate([
{
$match: {
fromAccountId: accountId,
type: 'withdrawal',
timestamp: {
$gte: new Date(today),
$lt: new Date(new Date(today).getTime() + 24 * 60 * 60 * 1000)
}
}
},
{
$group: {
_id: null,
totalWithdrawn: { $sum: '$amount' }
}
}
]);
const todayTotal = todayWithdrawals[0]?.totalWithdrawn || 0;
const dailyLimit = account.dailyWithdrawalLimit || 1000;
// RACE CONDITION: Multiple withdrawals can bypass daily limit
if (todayTotal + amount > dailyLimit) {
return res.status(400).json({ error: 'Daily withdrawal limit exceeded' });
}
// RACE CONDITION: Balance check and update not atomic
if (account.balance < amount) {
return res.status(400).json({ error: 'Insufficient funds' });
}
account.balance -= amount;
await account.save();
const transaction = await Transaction.create({
fromAccountId: accountId,
amount,
type: 'withdrawal',
status: 'completed',
timestamp: new Date()
});
res.json({
success: true,
transactionId: transaction.id,
newBalance: account.balance
});
} catch (error) {
res.status(500).json({ error: 'Withdrawal failed' });
}
});
// Attack scenarios:
// 1. Submit multiple simultaneous transfers from same account
// 2. Concurrent withdrawals to bypass daily limits
// 3. Rapid-fire transactions during balance checks
// 4. Race conditions during account closure/freeze
// 5. Concurrent credit/debit operations