Failed/rejected transactions logged with reason code
Why it matters
A failed authorization denial, insufficient-funds rejection, or rate-limit block is not a neutral event — it is a potential indicator of credential stuffing, account takeover, or fraud probing. If it is not logged with a reason code, your security team has no signal to detect these patterns. PCI-DSS 4.0 Req-10.2.1.7 explicitly requires logging of invalid logical access attempts. NIST 800-53 AU-2 requires logging of failed login attempts and failed authentication events. FINRA Rule 4511 requires complete books and records, which include rejected transactions. CWE-778 (Insufficient Logging) covers the failure to record security-relevant events. An attacker probing for accounts with insufficient-funds errors on micro-transactions to enumerate valid account IDs will leave no trace if rejections are silently swallowed.
Severity rationale
Low because failed-transaction logging is a detective control — its absence does not enable attacks directly, but it eliminates the forensic evidence needed to detect and investigate fraud after the fact.
Remediation
Wrap every failure path in src/services/transaction.ts with an explicit log write before throwing, and define a fixed set of reason codes as a TypeScript union or enum:
type FailReason =
| 'INSUFFICIENT_FUNDS'
| 'ACCOUNT_FROZEN'
| 'AUTHORIZATION_DENIED'
| 'INVALID_INPUT'
| 'RATE_LIMITED'
| 'ACCOUNT_NOT_FOUND';
async function logFailure(
userId: string, operation: string, amount: number, reason: FailReason
) {
await db('transaction_logs').insert({
user_id: userId, operation_type: operation, amount,
result: 'fail', reason_code: reason,
timestamp: new Date().toISOString()
});
}
// In executeTransfer:
if (account.balance < amount) {
await logFailure(userId, 'transfer', amount, 'INSUFFICIENT_FUNDS');
throw new TransactionError('Insufficient funds');
}
A catch-all block at the route level does not satisfy this requirement — each named failure condition needs its own reason_code recorded before the exception propagates.
Detection
-
ID:
failed-transaction-logging -
Severity:
low -
What to look for: Count all error/failure handling paths in financial transaction endpoints. For each failure path, check whether a log entry is written with a reason code. Enumerate the distinct reason codes used (e.g., INSUFFICIENT_FUNDS, AUTHORIZATION_DENIED, INVALID_INPUT). Quote the actual reason code values found. A catch block that silently swallows errors without logging does not count as pass.
-
Pass criteria: At least 90% of failure handling paths write to the audit log with a reason code. Count all distinct reason codes — at least 3 must exist. Report the ratio even on pass (e.g., "7 of 8 failure paths logged, 5 distinct reason codes: INSUFFICIENT_FUNDS, AUTH_DENIED, INVALID_INPUT, RATE_LIMITED, ACCOUNT_FROZEN").
-
Fail criteria: Failed transactions are not logged, or failure reasons are omitted from the audit trail, or fewer than 3 distinct reason codes exist.
-
Skip (N/A) when: Never — failures are regulatory events that must be tracked.
-
Detail on fail:
"Failed transactions logged in 2 of 6 failure paths — reason codes present in 0 paths."or"Authorization denials not logged — 0 of 3 auth failure paths write to audit log.". -
Remediation: Ensure all failures are logged with reasons (in
src/services/transaction.tsor equivalent):async function executeTransfer(userId, toAccountId, amount) { // Validation const account = await db.accounts.findOne({ userId }); if (!account) { await db.transactionLogs.create({ userId, operationType: 'transfer', amount, result: 'fail', reason: 'ACCOUNT_NOT_FOUND', timestamp: new Date().toISOString() }); throw new Error('Account not found'); } if (account.balance < amount) { await db.transactionLogs.create({ userId, operationType: 'transfer', amount, result: 'fail', reason: 'INSUFFICIENT_FUNDS', detail: `Balance: ${account.balance}, Requested: ${amount}`, timestamp: new Date().toISOString() }); throw new Error('Insufficient funds'); } // Execute transfer // ... success logging ... }
External references
- cwe · CWE-778 — Insufficient Logging
- nist:rev5 · AU-2 — Event Logging — log failed access attempts and errors
- pci-dss:4.0 · Req-10.2.1.7 — Log all invalid logical access attempts
- external · FINRA-Rule-4511 — FINRA Rule 4511 — records of all transactions including rejections
Taxons
History
- 2026-04-18·v1.0.0·Initial import from finserv-audit-trail·automated