Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
35 changes: 33 additions & 2 deletions lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ type Context = Record<string, unknown>;

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<string, unknown>);
} else {
result[key] = value;
}
}

return result;
}

const COLORS = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
Expand All @@ -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));
Expand All @@ -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}`;

Expand Down
Loading