From d166f099e87282493cf1fdad2c634d06d5f93bf7 Mon Sep 17 00:00:00 2001 From: tamilr0727-ux Date: Sun, 21 Jun 2026 14:04:03 +0530 Subject: [PATCH] fix(logger): redact sensitive fields in structured logging --- lib/logger.test.ts | 32 ++++++++++++++++++++++++++++++++ lib/logger.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/lib/logger.test.ts b/lib/logger.test.ts index 4f1d8aaf4..fcc9633a9 100644 --- a/lib/logger.test.ts +++ b/lib/logger.test.ts @@ -86,4 +86,36 @@ describe('logger', () => { expect(parsed.route).toBe('/api/test'); expect(parsed.timestamp).toBeDefined(); }); + + // ─── NEW SECURITY REDACTION TEST ─────────────────────────────────────────── + it('redacts sensitive fields in production logs', async () => { + process.env = { + ...process.env, + NODE_ENV: 'production', + }; + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const logger = (await import('./logger')).default; + + logger.error('sensitive data leak test', { + password: 'super_secret_password', + email: 'user@domain.com', + nested: { + token: 'secret_token_123', + safeField: 'cleartext', + }, + }); + + const output = logSpy.mock.calls[0][0] as string; + const parsed = JSON.parse(output); + + // Verify sensitive keys are safely masked + expect(parsed.password).toBe('[REDACTED]'); + expect(parsed.email).toBe('[REDACTED]'); + expect(parsed.nested.token).toBe('[REDACTED]'); + + // Verify non-sensitive keys pass through untouched + expect(parsed.nested.safeField).toBe('cleartext'); + }); }); diff --git a/lib/logger.ts b/lib/logger.ts index d7ddf2f42..f05156da9 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -2,6 +2,30 @@ type Context = Record; const isProduction = process.env.NODE_ENV === 'production'; +// Define sensitive keys that should be masked +const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'authorization', 'cookie', 'email']; + +/** + * Recursively scans and redacts sensitive information from an object. + */ +function redact(obj: Context): Context { + const result: Context = {}; + + for (const [key, value] of Object.entries(obj)) { + // Check if the current key contains any of our sensitive keywords + if (SENSITIVE_KEYS.some((k) => key.toLowerCase().includes(k))) { + result[key] = '[REDACTED]'; + } else if (typeof value === 'object' && value !== null) { + // Recursively redact nested objects + result[key] = redact(value as Record); + } else { + result[key] = value; + } + } + + return result; +} + const COLORS = { debug: '\x1b[36m', // Cyan info: '\x1b[32m', // Green @@ -15,11 +39,14 @@ function createTimestamp(): string { } function logProduction(level: 'warn' | 'error', msg: string, ctx: Context = {}): void { + // Redact sensitive fields before structure serialization + const redactedCtx = redact(ctx); + const payload = { level, msg, timestamp: createTimestamp(), - ...ctx, + ...redactedCtx, }; console.log(JSON.stringify(payload)); @@ -31,7 +58,11 @@ function logDevelopment( ctx: Context = {} ): void { const color = COLORS[level]; - const contextString = Object.keys(ctx).length > 0 ? ` ${JSON.stringify(ctx)}` : ''; + + // Also redact in development to prevent accidental terminal exposure + const redactedCtx = redact(ctx); + const contextString = + Object.keys(redactedCtx).length > 0 ? ` ${JSON.stringify(redactedCtx)}` : ''; const output = `${color}[${level.toUpperCase()}]${COLORS.reset} ${msg}${contextString}`;