Skip to content
Closed
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
61 changes: 52 additions & 9 deletions modules/logger/src/sanitizeLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SENSITIVE_KEYS = new Set([
'_token',
]);

const BEARER_V2_PATTERN = /^v2x[a-f0-9]{32,}$/i;
const BEARER_V2_PATTERN = /^v2x[a-fA-F0-9]{32,}$/;

/**
* Checks if a key is sensitive (case-insensitive)
Expand Down Expand Up @@ -50,7 +50,11 @@ export function getErrorData(error: unknown): unknown {

/**
* Recursively sanitizes an object, replacing sensitive values with '<REMOVED>'
* Handles circular references and nested structures
* Handles circular references, nested structures, and various JavaScript types
* @param obj - The value to sanitize
* @param seen - WeakSet to track circular references
* @param depth - Current recursion depth
* @returns Sanitized value
*/
export function sanitize(obj: unknown, seen = new WeakSet<Record<string, unknown>>(), depth = 0): unknown {
if (depth > 25) {
Expand All @@ -74,24 +78,63 @@ export function sanitize(obj: unknown, seen = new WeakSet<Record<string, unknown
return obj;
}

// Handle circular references
if (seen.has(obj as Record<string, unknown>)) {
// Handle Date objects before circular check (Dates should be converted, not tracked)
if (obj instanceof Date) {
return obj.toISOString();
}

// Handle circular references (only for objects that can be in WeakSet)
const objAsRecord = obj as Record<string, unknown>;
if (seen.has(objAsRecord)) {
return '[Circular]';
}

seen.add(obj as Record<string, unknown>);
seen.add(objAsRecord);

// Handle arrays
if (Array.isArray(obj)) {
return obj.map((item) => sanitize(item, seen, depth + 1));
}

// Handle Date objects
if (obj instanceof Date) {
return obj.toISOString();
// Handle RegExp
if (obj instanceof RegExp) {
return obj.toString();
}

// Handle Buffer (Node.js)
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) {
return '<Buffer>';
}

// Handle typed arrays
if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
return '<TypedArray>';
}

// Handle Map
if (obj instanceof Map) {
const sanitized = new Map();
for (const [key, value] of obj.entries()) {
const keyStr = typeof key === 'string' ? key : String(key);
if (isSensitiveKey(keyStr) || isBearerV2Token(value)) {
sanitized.set(key, '<REMOVED>');
} else {
sanitized.set(key, sanitize(value, seen, depth + 1));
}
}
return sanitized;
}

// Handle Set
if (obj instanceof Set) {
const sanitized = new Set();
for (const value of obj.values()) {
sanitized.add(sanitize(value, seen, depth + 1));
}
return sanitized;
}

// Handle objects
// Handle plain objects and other object types
const sanitized: Record<string, unknown> = {};

for (const [key, value] of Object.entries(obj)) {
Expand Down
113 changes: 113 additions & 0 deletions modules/logger/test/unit/sanitizeLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,103 @@ describe('sanitize', function () {
});
});

describe('RegExp handling', function () {
it('should convert RegExp to string', function () {
assert.strictEqual(sanitize(/test/gi), '/test/gi');
});

it('should handle RegExp in objects', function () {
const obj = { pattern: /[a-z]+/i, name: 'validator' };
const result = sanitize(obj) as any;
assert.strictEqual(result.pattern, '/[a-z]+/i');
assert.strictEqual(result.name, 'validator');
});
});

describe('Buffer handling', function () {
it('should handle Buffer objects', function () {
if (typeof Buffer !== 'undefined') {
const buf = Buffer.from('test data');
assert.strictEqual(sanitize(buf), '<Buffer>');
}
});

it('should handle Buffer in objects', function () {
if (typeof Buffer !== 'undefined') {
const obj = { data: Buffer.from('secret'), label: 'file' };
const result = sanitize(obj) as any;
assert.strictEqual(result.data, '<Buffer>');
assert.strictEqual(result.label, 'file');
}
});
});

describe('TypedArray handling', function () {
it('should handle Uint8Array', function () {
const arr = new Uint8Array([1, 2, 3]);
assert.strictEqual(sanitize(arr), '<TypedArray>');
});

it('should handle Int32Array in objects', function () {
const obj = { numbers: new Int32Array([10, 20, 30]), count: 3 };
const result = sanitize(obj) as any;
assert.strictEqual(result.numbers, '<TypedArray>');
assert.strictEqual(result.count, 3);
});
});

describe('Map handling', function () {
it('should sanitize Map entries', function () {
const map = new Map([
['user', 'alice'],
['password', 'secret'],
]);
const result = sanitize(map) as Map<any, any>;
assert.ok(result instanceof Map);
assert.strictEqual(result.get('user'), 'alice');
assert.strictEqual(result.get('password'), '<REMOVED>');
});

it('should handle nested Map in objects', function () {
const obj = {
config: new Map([
['host', 'localhost'],
['token', 'secret'],
]),
};
const result = sanitize(obj) as any;
assert.ok(result.config instanceof Map);
assert.strictEqual(result.config.get('host'), 'localhost');
assert.strictEqual(result.config.get('token'), '<REMOVED>');
});

it('should handle v2 token in Map values', function () {
const map = new Map([['auth', V2_TOKEN]]);
const result = sanitize(map) as Map<any, any>;
assert.strictEqual(result.get('auth'), '<REMOVED>');
});
});

describe('Set handling', function () {
it('should sanitize Set entries', function () {
const set = new Set(['user1', V2_TOKEN, 'user3']);
const result = sanitize(set) as Set<any>;
assert.ok(result instanceof Set);
assert.strictEqual(result.has('user1'), true);
assert.strictEqual(result.has('<REMOVED>'), true);
assert.strictEqual(result.has('user3'), true);
assert.strictEqual(result.has(V2_TOKEN), false);
});

it('should handle nested Set in objects', function () {
const obj = { tags: new Set(['public', 'private']) };
const result = sanitize(obj) as any;
assert.ok(result.tags instanceof Set);
assert.strictEqual(result.tags.has('public'), true);
assert.strictEqual(result.tags.has('private'), true);
});
});

describe('mixed complex object', function () {
it('should handle all data types together', function () {
const complexObj = {
Expand All @@ -270,6 +367,22 @@ describe('sanitize', function () {
assert.strictEqual(result.tags[0], 'transfer');
assert.strictEqual(result.tags[1], 'urgent');
});

it('should handle all new data types together', function () {
const complexObj = {
pattern: /test/gi,
bigNum: 999n,
tags: new Set(['a', 'b']),
config: new Map([['key', 'value']]),
date: new Date('2026-01-01T00:00:00.000Z'),
};
const result = sanitize(complexObj) as any;
assert.strictEqual(result.pattern, '/test/gi');
assert.strictEqual(result.bigNum, '999');
assert.ok(result.tags instanceof Set);
assert.ok(result.config instanceof Map);
assert.strictEqual(result.date, '2026-01-01T00:00:00.000Z');
});
});
});

Expand Down