An append-only table that cannot be modified by the application is still vulnerable to a privileged database administrator who directly connects to the instance and runs UPDATE or DELETE without going through the application layer. Tamper evidence via hash chains or digital signatures closes that gap — any modification of a stored log entry invalidates the chain and is detectable even after the fact. NIST 800-53 AU-10 (Non-Repudiation) requires that the organization protect against individuals falsely denying having performed particular actions. NIST 800-53 AU-9 requires protection of audit information integrity. PCI-DSS v4.0 Req-10.3.4 requires file-integrity monitoring or change-detection mechanisms on audit logs so existing data cannot be changed without generating alerts — exactly the property hash chains or digital signatures provide. NIST SP 800-92 (Guide to Computer Security Log Management) recommends integrity checking of log archives. CWE-345 (Insufficient Verification of Data Authenticity) is the vulnerability class.
Medium because tamper detection is a secondary control — without it, a privileged insider can modify log records directly at the database layer and the breach may go undetected indefinitely.
Add previous_hash and entry_hash columns, then compute the hash in the logging service in src/services/audit-logger.ts:
import crypto from 'crypto';
async function logWithHashChain(entry: AuditEntry) {
const previous = await db('transaction_logs')
.orderBy('created_at', 'desc').first(['entry_hash']);
const payload = JSON.stringify({
user_id: entry.userId, operation: entry.operation,
amount: entry.amount, timestamp: entry.timestamp,
previous_hash: previous?.entry_hash ?? 'GENESIS'
});
const entryHash = crypto
.createHmac('sha256', process.env.AUDIT_SIGNING_KEY!)
.update(payload)
.digest('hex');
await db('transaction_logs').insert({
...entry,
previous_hash: previous?.entry_hash ?? null,
entry_hash: entryHash
});
}
Store AUDIT_SIGNING_KEY in a secrets manager (AWS Secrets Manager, Vault), not in the application's environment file.
ID: finserv-audit-trail.retention-compliance.tamper-evidence
Severity: medium
What to look for: Count all cryptographic integrity columns in the audit log table (e.g., entry_hash, previous_hash, signature). Quote the actual column names and hash algorithm used. Examine logging code for cryptographic signing or hashing operations — count every code path that computes a hash or signature. A table without any hash/signature columns does not count as pass — do not pass if no cryptographic integrity mechanism exists.
Pass criteria: Audit log entries include at least 1 cryptographic integrity column (hash chain or digital signature). At least 1 verification function exists that can detect tampering. Quote the actual hash algorithm used (e.g., SHA-256, HMAC-SHA256). Report the count even on pass (e.g., "2 integrity columns found: entry_hash (SHA-256), previous_hash").
Fail criteria: No tamper-detection mechanism found — 0 cryptographic columns in audit log. Audit logs are stored without cryptographic integrity checks.
Skip (N/A) when: Never — tamper evidence is a regulatory requirement for financial systems.
Detail on fail: "Audit logs are stored as plain records. No hash chain or digital signatures are used to detect tampering.".
Remediation: Implement a hash chain:
ALTER TABLE transaction_logs ADD COLUMN previous_hash VARCHAR(256);
ALTER TABLE transaction_logs ADD COLUMN entry_hash VARCHAR(256) GENERATED ALWAYS AS (
SHA256(CONCAT(id, timestamp, user_id, operation_type, COALESCE(previous_hash, '')))
) STORED;
Or use digital signatures:
import crypto from 'crypto';
async function logTransaction(userId, operation, amount) {
const signature = crypto
.createHmac('sha256', process.env.AUDIT_KEY)
.update(JSON.stringify({ userId, operation, amount, timestamp: new Date().toISOString() }))
.digest('hex');
await db.transactionLogs.create({
userId,
operationType: operation,
amount,
signature, // Include in record
timestamp: new Date().toISOString()
});
}
// Verify integrity
function verifyLog(logEntry, key) {
const expectedSig = crypto
.createHmac('sha256', key)
.update(JSON.stringify({
userId: logEntry.userId,
operation: logEntry.operationType,
amount: logEntry.amount,
timestamp: logEntry.timestamp
}))
.digest('hex');
return logEntry.signature === expectedSig;
}