fix: critical security improvements
- Apply optimistic locking to deposit-matcher.ts (race condition fix) - Add timing-safe comparison for API key validation - Move admin IDs from wrangler.toml vars to secrets - Add .env.example for secure credential management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { BankNotification } from '../types';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { executeWithOptimisticLock, OptimisticLockError } from '../utils/optimistic-lock';
|
||||
|
||||
const logger = createLogger('deposit-matcher');
|
||||
|
||||
@@ -61,33 +62,51 @@ export async function matchPendingDeposit(
|
||||
console.log('[matchPendingDeposit] 매칭 발견:', pendingTx);
|
||||
|
||||
try {
|
||||
// 트랜잭션: 거래 확정 + 잔액 증가 + 알림 매칭 업데이트
|
||||
const results = await db.batch([
|
||||
db.prepare(
|
||||
"UPDATE deposit_transactions SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?"
|
||||
).bind(pendingTx.id),
|
||||
db.prepare(
|
||||
'UPDATE user_deposits SET balance = balance + ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||
).bind(pendingTx.amount, pendingTx.user_id),
|
||||
db.prepare(
|
||||
'UPDATE bank_notifications SET matched_transaction_id = ? WHERE id = ?'
|
||||
).bind(pendingTx.id, notificationId),
|
||||
]);
|
||||
// Optimistic Locking으로 동시성 안전한 매칭 처리
|
||||
await executeWithOptimisticLock(db, async (attempt) => {
|
||||
logger.info('SMS 자동 매칭 시도', { attempt, transactionId: pendingTx.id });
|
||||
|
||||
// Batch 결과 검증
|
||||
const allSuccessful = results.every(r => r.success && r.meta?.changes && r.meta.changes > 0);
|
||||
if (!allSuccessful) {
|
||||
logger.error('Batch 부분 실패 (입금 자동 매칭 - SMS)', undefined, {
|
||||
results,
|
||||
userId: pendingTx.user_id,
|
||||
// 1. 거래 상태 변경 (pending → confirmed)
|
||||
const txUpdate = await db.prepare(
|
||||
"UPDATE deposit_transactions SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'"
|
||||
).bind(pendingTx.id).run();
|
||||
|
||||
if (!txUpdate.success || txUpdate.meta.changes === 0) {
|
||||
// 이미 다른 프로세스가 처리함 (중복 매칭 방지)
|
||||
throw new Error('Transaction already processed or not found');
|
||||
}
|
||||
|
||||
// 2. 현재 잔액/버전 조회
|
||||
const current = await db.prepare(
|
||||
'SELECT balance, version FROM user_deposits WHERE user_id = ?'
|
||||
).bind(pendingTx.user_id).first<{ balance: number; version: number }>();
|
||||
|
||||
if (!current) {
|
||||
throw new Error('User deposit account not found');
|
||||
}
|
||||
|
||||
// 3. 잔액 증가 (버전 체크로 동시성 보호)
|
||||
const balanceUpdate = await db.prepare(
|
||||
'UPDATE user_deposits SET balance = balance + ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND version = ?'
|
||||
).bind(pendingTx.amount, pendingTx.user_id, current.version).run();
|
||||
|
||||
if (!balanceUpdate.success || balanceUpdate.meta.changes === 0) {
|
||||
throw new OptimisticLockError('Version mismatch on balance update');
|
||||
}
|
||||
|
||||
// 4. 알림과 거래 연결
|
||||
await db.prepare(
|
||||
'UPDATE bank_notifications SET matched_transaction_id = ? WHERE id = ?'
|
||||
).bind(pendingTx.id, notificationId).run();
|
||||
|
||||
logger.info('SMS 자동 매칭 완료', {
|
||||
attempt,
|
||||
transactionId: pendingTx.id,
|
||||
userId: pendingTx.user_id,
|
||||
amount: pendingTx.amount,
|
||||
notificationId,
|
||||
depositorName: notification.depositorName,
|
||||
context: 'match_pending_deposit_sms'
|
||||
newBalance: current.balance + pendingTx.amount,
|
||||
});
|
||||
throw new Error('거래 처리 실패 - 관리자에게 문의하세요');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[matchPendingDeposit] 매칭 완료:', {
|
||||
transactionId: pendingTx.id,
|
||||
@@ -101,6 +120,12 @@ export async function matchPendingDeposit(
|
||||
amount: pendingTx.amount,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof OptimisticLockError) {
|
||||
logger.error('SMS 자동 매칭 동시성 충돌', error, {
|
||||
transactionId: pendingTx.id,
|
||||
userId: pendingTx.user_id,
|
||||
});
|
||||
}
|
||||
console.error('[matchPendingDeposit] DB 업데이트 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user