fix: prevent sensitive data logging (#70)

* feat: Add sensitive data masking to packet logging by introducing a sanitization function and a list of sensitive keys.

* test: mock console methods in logger tests and restore after each test

* refactor: remove redundant sensitive data masking functions and integrate safeStringifyPacket utility for improved logging

* fix: run format:write

* test: enhance logger tests with error handling for directory setup and cleanup

* refactor: optimize sensitive data masking logic by checking for exact matches in sensitive keys

* refactor: enhance sensitive data masking to handle circular references and improve sanitization logic

* refactor: update sensitive data masking comments and test descriptions to improve clarity and consistency

* test: enhance logger tests with dynamic log directory creation and cleanup after tests

* test: add unit tests for handling malformed JSON strings in sensitive data masking
This commit is contained in:
Riccardo Venturini authored and GitHub committed 2026-02-08 14:31:04 +08:00
1 parent 56137092be
commit 5155e37fd1
5 files changed
+238 -22

No files matched your search

+32 -2
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
import { logger } from '../../utils/logger';
import fs from 'fs';
import path from 'path';
@@ -42,8 +42,38 @@ describe('Logger Utilities', () => {
};
beforeEach(() => {
if (!fs.existsSync(testLogDir)) {
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
try {
if (fs.existsSync(testLogDir)) {
fs.rmSync(testLogDir, { recursive: true, force: true });
}
fs.mkdirSync(testLogDir, { recursive: true });
} catch (err) {
console.error('beforeEach: setup testLogDir failed', err);
throw err;
}
});
afterEach(() => {
vi.restoreAllMocks();
try {
if (fs.existsSync(testLogDir)) {
fs.rmSync(testLogDir, { recursive: true, force: true });
}
} catch (err) {
console.error('afterEach: cleanup testLogDir failed', err);
throw err;
}
});
afterAll(() => {
try {
if (fs.existsSync(testLogDir)) {
fs.rmSync(testLogDir, { recursive: true, force: true });
}
} catch (err) {
console.error('afterAll: cleanup testLogDir failed', err);
}
});
@@ -0,0 +1,96 @@
import { describe, it, expect } from 'vitest';
import { sanitizeObject, safeStringifyPacket } from '@/utils/sensitiveDataMasking';
describe('sensitive data masking', () => {
describe('sanitizeObject', () => {
it('masks password', () => {
expect(sanitizeObject({ password: 'secret123' })).toEqual({ password: '[REDACTED]' });
});
it('masks token and variants (case-insensitive)', () => {
expect(sanitizeObject({ token: 'abc' })).toEqual({ token: '[REDACTED]' });
expect(sanitizeObject({ Authorization: 'Bearer xyz' })).toEqual({
Authorization: '[REDACTED]',
});
expect(sanitizeObject({ api_key: 'key123' })).toEqual({ api_key: '[REDACTED]' });
expect(sanitizeObject({ refresh_token: 'rt' })).toEqual({ refresh_token: '[REDACTED]' });
});
it('masks nested keys', () => {
expect(
sanitizeObject({
user: { name: 'Alice', password: 'pwd', nested: { token: 't' } },
}),
).toEqual({
user: { name: 'Alice', password: '[REDACTED]', nested: { token: '[REDACTED]' } },
});
});
it('does not mask non-sensitive keys', () => {
expect(sanitizeObject({ name: 'Alice', id: 1 })).toEqual({ name: 'Alice', id: 1 });
});
it('handles null and undefined', () => {
expect(sanitizeObject(null)).toBe(null);
expect(sanitizeObject(undefined)).toBe(undefined);
});
it('handles arrays recursively', () => {
expect(sanitizeObject([{ password: 'x' }, { name: 'y' }])).toEqual([
{ password: '[REDACTED]' },
{ name: 'y' },
]);
});
it('handles JSON string with sensitive fields', () => {
const json = JSON.stringify({ password: 'hidden' });
expect(sanitizeObject(json)).toBe(JSON.stringify({ password: '[REDACTED]' }));
});
it('leaves non-JSON string unchanged', () => {
expect(sanitizeObject('plain text')).toBe('plain text');
});
it('leaves malformed JSON string unchanged (no throw)', () => {
expect(sanitizeObject('{"broken":')).toBe('{"broken":');
expect(sanitizeObject('not valid json ]')).toBe('not valid json ]');
});
it('handles circular references without infinite loops', () => {
const circular: Record<string, unknown> = { name: 'a', password: 'secret' };
circular.self = circular;
expect(sanitizeObject(circular)).toEqual({
name: 'a',
password: '[REDACTED]',
self: '[Circular]',
});
});
it('masks session_id, cookie, client_secret, otp, pin', () => {
expect(
sanitizeObject({
session_id: 'sess',
cookie: 'c',
client_secret: 'cs',
otp: '1234',
pin: '0000',
}),
).toEqual({
session_id: '[REDACTED]',
cookie: '[REDACTED]',
client_secret: '[REDACTED]',
otp: '[REDACTED]',
pin: '[REDACTED]',
});
});
});
describe('safeStringifyPacket', () => {
it('stringifies with sensitive fields masked', () => {
const out = safeStringifyPacket({ user: 'a', password: 'p' });
expect(out).toContain('"user":"a"');
expect(out).toContain('"password":"[REDACTED]"');
expect(() => JSON.parse(out)).not.toThrow();
});
});
});