security: round 3 hardening (CSRF double-submit, TRX MITM, container hardening)

This commit is contained in:
ZOMBIIIIIII
2026-05-12 01:47:58 +03:00
parent c8bc40af97
commit 8dc0855827
37 changed files with 1852 additions and 318 deletions

View File

@@ -0,0 +1,75 @@
/**
* Audit log — append-only JSON lines в `logs/audit.log`.
* Используется для критических custodial операций.
* НИКОГДА не логирует mnemonic / privkey / encrypted blob.
*/
import { promises as fs } from 'fs';
import path from 'path';
import { logger } from './logger';
import { getTraceId } from './trace-store';
const AUDIT_DIR = path.resolve(__dirname, '../../../../logs');
const AUDIT_FILE = path.join(AUDIT_DIR, 'audit.log');
let initialized = false;
async function ensureFile(): Promise<void> {
if (initialized) return;
try {
await fs.mkdir(AUDIT_DIR, { recursive: true });
const handle = await fs.open(AUDIT_FILE, 'a', 0o600);
await handle.close();
try {
await fs.chmod(AUDIT_FILE, 0o600);
} catch {
// Windows chmod — игнор
}
initialized = true;
} catch (err: any) {
logger.error(`Audit log init failed: ${err.message}`);
}
}
export interface AuditEntry {
event: string;
userId: string;
ip?: string | null;
meta?: Record<string, unknown>;
result?: 'success' | 'failure';
errorCode?: string;
}
/**
* Best-effort write. Если запись провалилась — только log, не throws.
* Используется для не-критических событий (wallet.create success, etc).
*/
export async function auditLog(entry: AuditEntry): Promise<void> {
await ensureFile();
const line = JSON.stringify({
timestamp: new Date().toISOString(),
trace_id: getTraceId(),
...entry,
});
try {
await fs.appendFile(AUDIT_FILE, line + '\n', { mode: 0o600 });
} catch (err: any) {
logger.error(`Audit log write failed: ${err.message}`);
}
}
/**
* Fail-secure write. Если запись провалилась — throws.
* Используется для critical security событий (mnemonic.reveal, wallet.send),
* где compliance требует чтобы операция НЕ происходила без audit-trail.
*/
export async function auditLogStrict(entry: AuditEntry): Promise<void> {
await ensureFile();
const line = JSON.stringify({
timestamp: new Date().toISOString(),
trace_id: getTraceId(),
...entry,
});
// Без try/catch — caller обрабатывает failure
await fs.appendFile(AUDIT_FILE, line + '\n', { mode: 0o600 });
}

View File

@@ -35,14 +35,14 @@ function getCallerInfo(): { file: string; line: number } {
const SENSITIVE_PATTERNS: { regex: RegExp; replace: string }[] = [
{ regex: /(password|passwd|pwd)\s*[:=]\s*\S+/gi, replace: '$1=***' },
{ regex: /(token|secret|api[_-]?key|auth)\s*[:=]\s*\S+/gi, replace: '$1=***' },
{ regex: /(role[_-]?id|secret[_-]?id)\s*[:=]\s*\S+/gi, replace: '$1=***' },
{ regex: /(mnemonic|seed[_-]?phrase|private[_-]?key|priv[_-]?key)\s*[:=]\s*\S+/gi, replace: '$1=***' },
{ regex: /\bhvs\.[A-Za-z0-9]+/g, replace: 'hvs.***' },
{ regex: /\bs\.[A-Za-z0-9]{20,}/g, replace: 's.***' },
{ regex: /Bearer\s+[A-Za-z0-9._-]+/gi, replace: 'Bearer ***' },
{ regex: /Authorization:\s*\S+/gi, replace: 'Authorization: ***' },
// BIP39 mnemonic phrase (12+ lowercase английских слов через пробел) — рискованный паттерн,
// но лучше пере-санитайзить чем пропустить mnemonic в логи
{ regex: /\b([a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g, replace: '[REDACTED_MNEMONIC]' },
// BIP39 mnemonic phrase (12-24 lowercase английских слов через пробел) — case-insensitive
{ regex: /\b([a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/gi, replace: '[REDACTED_MNEMONIC]' },
// Hex privkey (64 hex chars подряд, optional 0x)
{ regex: /\b(0x)?[0-9a-fA-F]{64}\b/g, replace: '[REDACTED_HEX64]' },
];