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
4 changes: 4 additions & 0 deletions modules/express/bin/bitgo-express
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ process.on('unhandledRejection', (reason, promise) => {

const { init } = require('../dist/src/expressApp');

// Initialize console sanitization
const { overrideConsole } = require('../dist/src/utils/consoleOverride');
overrideConsole();

if (require.main === module) {
init().catch((err) => {
console.log(`Fatal error: ${err.message}`);
Expand Down
3 changes: 2 additions & 1 deletion modules/express/src/expressApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import timeout from 'connect-timeout';
import * as bodyParser from 'body-parser';

import { Config, config } from './config';

// Initialize console sanitization (ensures override for TypeScript entry point)
import './utils/consoleOverride';
const debug = debugLib('bitgo:express');

import { SSL_OP_NO_TLSv1 } from 'constants';
Expand Down
108 changes: 108 additions & 0 deletions modules/express/src/utils/consoleOverride.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @prettier
*/

import { sanitize } from './sanitizeLog';

// Store original console methods for restoration if needed
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
debug: console.debug,
info: console.info,
dir: console.dir,
table: console.table,
trace: console.trace,
assert: console.assert,
};

/**
* Override global console methods to sanitize sensitive data
* This intercepts all console calls and removes sensitive fields before logging
*/
export function overrideConsole(): void {
// Standard logging methods
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
console.log = function (...args: any[]) {
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.log.apply(console, sanitizedArgs);
};

console.error = function (...args: any[]) {
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.error.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);
};

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

// Special methods with specific handling

/**
* console.dir(obj, options)
* Second argument is options, not data - don't sanitize it
*/
console.dir = function (obj: any, options?: any) {
const sanitizedObj = sanitize(obj);
originalConsole.dir.call(console, sanitizedObj, options);
};

/**
* console.table(data, properties)
* Second argument is column selection - don't sanitize it
*/
console.table = function (data: any, properties?: string[]) {
const sanitizedData = sanitize(data);
originalConsole.table.call(console, sanitizedData, properties);
};

/**
* console.trace(...args)
* Prints stack trace with optional message/data
*/
console.trace = function (...args: any[]) {
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.trace.apply(console, sanitizedArgs);
};

/**
* console.assert(condition, ...args)
* First argument is boolean condition - don't sanitize it
*/
console.assert = function (condition?: boolean, ...args: any[]) {
const sanitizedArgs = args.map((arg) => sanitize(arg));
originalConsole.assert.call(console, condition, ...sanitizedArgs);
};
/* eslint-enable no-console, @typescript-eslint/no-explicit-any */
}

/**
* Restore original console methods
* Useful for testing or if sanitization needs to be disabled
*/
export function restoreConsole(): void {
/* eslint-disable no-console */
console.log = originalConsole.log;
console.error = originalConsole.error;
console.warn = originalConsole.warn;
console.debug = originalConsole.debug;
console.info = originalConsole.info;
console.dir = originalConsole.dir;
console.table = originalConsole.table;
console.trace = originalConsole.trace;
console.assert = originalConsole.assert;
/* eslint-enable no-console */
}
99 changes: 99 additions & 0 deletions modules/express/src/utils/sanitizeLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @prettier
*/

/**
* List of sensitive keywords to detect and remove from logs
* Uses case-insensitive substring matching
*/
const SENSITIVE_KEYWORDS = [
// Authentication & Authorization Tokens
'token', // Catches: token, _token, accessToken, refreshToken, authToken, bearer_token, authtoken, etc.
'bearer', // Catches: bearer, Bearer, bearer_token
'authorization', // Catches: authorization, Authorization
'authkey', // Catches: authKey, _authKey, myAuthKey
'oauth', // Catches: oauth, oAuth, oauth_token, oauth_client

// Client Credentials
'client', // Catches: client, client_id, client_secret, clientSecret, clientId, oauth_client

// Private Keys & Cryptographic Material
'prv', // Catches: prv, xprv, encryptedPrv
'privatekey', // Catches: privateKey, userPrivateKey, backupPrivateKey, rootPrivateKey, encryptedPrivateKey
'secret', // Catches: secret, _secret, clientSecret, secretKey, apiSecretKey

// Keychains
'keychain', // Catches: keychain, keychains, userKeychain

// Passwords & Passphrases
'password', // Catches: password, _password, userPassword, walletPassword
'passwd', // Catches: passwd, _passwd
'passphrase', // Catches: passphrase, walletPassphrase

// Recovery & Seeds
'mnemonic', // Catches: mnemonic, mnemonicPhrase
'seed', // Catches: seed, seedPhrase, userSeed

// Signatures & OTP
'signature', // Catches: signature, txSignature, walletSignature
'otp', // Catches: otp, otpCode, totpSecret

// API Keys
'apikey', // Catches: apiKey, apiKeyValue, myApiKey
];

/**
* 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
*/
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;
}

// Max depth protection to prevent stack overflow
if (depth > MAX_DEPTH) {
return '[Max Depth]';
}

// Handle primitives (string, number, boolean)
if (typeof data !== 'object') {
return data;
}

// Circular reference detection
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
const sanitized: any = {};
for (const key in data) {
if (data.hasOwnProperty(key)) {
const lowerKey = key.toLowerCase();

// Check if key contains any sensitive keyword (substring matching)
const isSensitive = SENSITIVE_KEYWORDS.some((keyword) => lowerKey.includes(keyword));

if (!isSensitive) {
// Safe field - recursively sanitize value
sanitized[key] = sanitize(data[key], seen, depth + 1);
}
// Sensitive fields are skipped (removed from output)
}
}

return sanitized;
}
Loading
Loading