Corrections/reversals logged separately with approval chain
Why it matters
When a financial correction or reversal is handled by deleting or modifying the original log record, the audit trail loses continuity — an examiner reviewing the log will see the final state but not the error and its correction, which is exactly the information they need to evaluate whether the process is operating with integrity. SOX §404 requires that corrections to financial records follow a controlled, documented process. NIST 800-53 AU-10 (Non-Repudiation) requires that the organization protect against individuals falsely denying having performed particular actions — an undocumented reversal creates the ability to deny that an original transaction ever occurred. FINRA Rule 4511 requires that corrections be documented as corrections, not as silent replacements. CWE-284 (Improper Access Control) covers the case where any operator can issue a reversal without a separate approver checking the request.
Severity rationale
Low because correction logging is a process-integrity control — its absence enables unilateral reversals without a second-party check, but the financial impact depends on the specific transaction value involved.
Remediation
Implement corrections as a two-record pattern in src/services/corrections.ts: one record for the approval event, one for the reversal transaction, neither touching the original row:
async function approveAndReverseTransaction(
correctionId: string, approverUserId: string
) {
const correction = await db('corrections').where({ id: correctionId }).first();
await db('transaction_logs').insert([
{
user_id: approverUserId,
operation_type: 'correction_approved',
details: JSON.stringify({
correction_id: correctionId,
original_transaction_id: correction.original_transaction_id,
reason: correction.reason,
requested_by: correction.requested_by
}),
timestamp: new Date().toISOString()
},
{
user_id: approverUserId,
operation_type: 'reversal',
details: JSON.stringify({
original_transaction_id: correction.original_transaction_id,
approved_by: approverUserId,
approved_at: new Date().toISOString()
}),
timestamp: new Date().toISOString()
}
]);
}
Verify requester_id !== approver_id before calling this function — a single person should never be able to self-approve a correction.
Detection
-
ID:
correction-approval-chain -
Severity:
low -
What to look for: Count all reversal/correction endpoints. For each, verify the correction is logged as a new operation (not a deletion of the original). Count the approval workflow fields: at minimum requester, approver, approval_timestamp, and original_transaction_id must be present. Quote the actual operation_type values used for corrections. Deleting the original transaction record does not count as pass — do not pass if any correction path deletes or modifies the original log entry.
-
Pass criteria: Corrections or reversals are logged as new operations with at least 4 fields: original_transaction_id reference, requester, approver, and approval_timestamp. At least 1 documented approval workflow requires a separate approver role. Report the count even on pass (e.g., "2 correction endpoints found, both log 5 approval fields").
-
Fail criteria: Corrections are applied without logging, or reversals are handled as deletions, or fewer than 4 approval fields are recorded, or no approval workflow exists.
-
Skip (N/A) when: Never — reversals are critical audit trail events.
-
Detail on fail:
"Reversals logged but 0 of 4 approval fields present — any operator can reverse without approval."or"Reversed transactions deleted from log — original record modified instead of new correction entry.". -
Remediation: Implement correction logging with approval (in
src/services/corrections.tsorsrc/app/api/corrections/route.ts):async function requestCorrection(originalTransactionId, reason) { const correction = await db.corrections.create({ originalTransactionId, reason, requestedBy: userId, requestedAt: new Date(), status: 'pending_approval' }); // Notify compliance for approval await notificationService.requestApproval(correction.id); } async function approveCorrection(correctionId, approverUserId) { const correction = await db.corrections.findById(correctionId); // Log the approval await db.transactionLogs.create({ userId: approverUserId, operationType: 'correction_approved', details: { originalTransactionId: correction.originalTransactionId, correctionId: correction.id, reason: correction.reason, approvedAt: new Date() } }); // Execute reversal as new transaction await db.transactionLogs.create({ userId: approverUserId, operationType: 'reversal', details: { originalTransactionId: correction.originalTransactionId, reversalReason: correction.reason } }); correction.status = 'approved'; correction.approvedBy = approverUserId; correction.approvedAt = new Date(); await correction.save(); }
External references
- cwe · CWE-284 — Improper Access Control — reversal without authorization
- nist:rev5 · AU-10 — Non-Repudiation — correction approval chain
- sox · Section 404 — Management Assessment of Internal Controls — approval workflows for adjustments
- external · FINRA-Rule-4511 — FINRA Rule 4511 — corrections must be documented separately from originals
Taxons
History
- 2026-04-18·v1.0.0·Initial import from finserv-audit-trail·automated