Skip to content
Draft
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
1 change: 1 addition & 0 deletions modules/express/src/expressApp.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import timeout from 'connect-timeout'
import * as bodyParser from 'body-parser'

import{Config, config } from './config'
import './utils/consoleOverride' // Auto-activates console sanitization in production

const debug = debugLib('bitgo:express');

Expand Down
56 changes: 56 additions & 0 deletions modules/express/src/utils/consoleOverride.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
/**
* @prettier
*/

import{sanitize } from './sanitizeLog'

// Store original console methods (only the ones we override)
/* eslint-disable no-console */
const originalConsole ={
error: console.error,
log: console.log,
warn: console.warn,
info: console.info,
};
/* eslint-enable no-console */

export function overrideConsole(): void{
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */

console.error = function (...args: any[]){
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.error.apply(console, sanitizedArgs);
};

console.log = function (...args: any[]){
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.log.apply(console, sanitizedArgs);
};

console.warn = function (...args: any[]){
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.warn.apply(console, sanitizedArgs);
};

console.info = function (...args: any[]){
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.info.apply(console, sanitizedArgs);
};

/* eslint-enable no-console, @typescript-eslint/no-explicit-any */
}
/**
* Restore original console methods (only the ones we overrode)
*/
export function restoreConsole(): void{
/* eslint-disable no-console */
console.error = originalConsole.error;
console.log = originalConsole.log;
console.warn = originalConsole.warn;
console.info = originalConsole.info;
/* eslint-enable no-console */
}
// Auto-activate console sanitization in testing and staging
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'staging'){
overrideConsole();
}
71 changes: 71 additions & 0 deletions modules/express/src/utils/sanitizeLog.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
/**
* @prettier
*/
/**
* Set of sensitive keywords for exact key matching (case-insensitive).
* Only matches exact key names (e.g., 'token' matches only 'token', not 'authtoken' or '_token').
* Using Set for O(1) lookup performance.
*/
const SENSITIVE_KEYS = new Set(['token', 'bearer', 'prv', 'privatekey', 'password', 'otp']);

/**
* Pattern to detect bearer v2 token values (e.g., v2xea99e123bba182f1360ad35529a7a6ae77cfc0bc4e5dcb4f88a6dd4e4bf6a8db)
* Matches strings starting with v2x followed by at least 32 hexadecimal characters
*/
const BEARER_V2_PATTERN = /^v2x[a-f0-9]{32,}$/i;

/**
* Recursively sanitize data by removing sensitive fields
* @param data - The data to sanitize
* @param seen - WeakSet to track circular references
* @param depth - Current recursion depth
* @returns Sanitized data with sensitive fields removed
*/

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export function sanitize(data: any, seen: WeakSet<Record<string, unknown>> = new WeakSet(), depth = 0): any{
const MAX_DEPTH = 50;

// Handle null/undefined
if (data === null || data === undefined){
return data;
}
// Prevent stack overflow
if (depth > MAX_DEPTH){
return '[Max Depth]'
}
// Handle primitives
if (typeof data !== 'object'){
// Check if string value is a bearer v2 token
if (typeof data === 'string' && BEARER_V2_PATTERN.test(data)){
return '<REMOVED>'
}
return data;
}
// Handle circular references
if (seen.has(data)){
return '[Circular]'
}
seen.add(data);

// Handle arrays
if (Array.isArray(data)){
return data.map((item) => sanitize(item, seen, depth + 1));
}
// Handle objects - replace sensitive field values with <REMOVED>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sanitized: any ={};
for (const key in data){
if (data.hasOwnProperty(key)){
// Check if key exactly matches any sensitive keyword (case-insensitive)
if (SENSITIVE_KEYS.has(key.toLowerCase())){
// Keep the field but replace value with <REMOVED>
sanitized[key] = '<REMOVED>'
} else{
// Recursively sanitize non-sensitive fields
sanitized[key] = sanitize(data[key], seen, depth + 1);
}
}
}
return sanitized;
}
Loading
Loading