🔐 fix: Redact Metadata in debugTraverse for Warn and Error

Relaxing the debug-only gate in debugTraverse (in commit 59371be0)
routed warn/error records through the traversal path, which emits leaf
string values verbatim (via truncateLongStrings only). Because
DEBUG_LOGGING defaults to true, those records are also written to the
rotating debug log file — which means payloads like
`{ auth: 'Bearer ...' }` or `{ openaiKey: 'sk-...' }` were persisted
unredacted once my earlier change took effect.

Apply redactMessage to the final formatted string when the level is
warn or error. Debug-level behavior is unchanged (matching prior art).

Includes regression tests covering error/warn redaction and
debug-level preservation.

Reviewed-by: Codex (P1 finding on PR #12737, commit e288f7fd).
This commit is contained in:
Danny Avila 2026-04-18 11:24:17 -04:00
parent e288f7fda7
commit c09d293d47
2 changed files with 55 additions and 10 deletions

View file

@ -1,4 +1,7 @@
const { formatConsoleMeta, redactMessage } = jest.requireActual('../parsers');
jest.unmock('winston');
const { formatConsoleMeta, redactMessage, debugTraverse } = jest.requireActual('../parsers');
const SPLAT_SYMBOL = Symbol.for('splat');
describe('formatConsoleMeta', () => {
it('returns empty string when there is no user metadata', () => {
@ -134,3 +137,44 @@ describe('redactMessage', () => {
expect(redactMessage(undefined)).toBe('');
});
});
describe('debugTraverse', () => {
const runFormatter = (info) => {
const transformed = debugTraverse.transform(info);
const MESSAGE = Symbol.for('message');
if (transformed && typeof transformed === 'object') {
return transformed[MESSAGE] ?? String(transformed);
}
return String(transformed);
};
const buildInfo = (level, meta) => {
const info = {
level,
message: 'test',
timestamp: 'ts',
...meta,
};
info[SPLAT_SYMBOL] = [meta];
return info;
};
it('redacts sensitive strings in metadata for error level', () => {
const out = runFormatter(buildInfo('error', { auth: 'Bearer eyJabc123', openai: 'sk-abc123' }));
expect(out).not.toContain('eyJabc123');
expect(out).not.toContain('sk-abc123');
expect(out).toContain('Bearer [REDACTED]');
expect(out).toContain('sk-[REDACTED]');
});
it('redacts sensitive strings in metadata for warn level', () => {
const out = runFormatter(buildInfo('warn', { header: 'Bearer supersecrettoken' }));
expect(out).not.toContain('supersecrettoken');
expect(out).toContain('Bearer [REDACTED]');
});
it('preserves debug-level metadata unmodified (existing behavior)', () => {
const out = runFormatter(buildInfo('debug', { someField: 'not-sensitive' }));
expect(out).toContain('not-sensitive');
});
});

View file

@ -164,30 +164,31 @@ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...met
}
let msg = `${timestamp} ${level}: ${truncateLongStrings(message?.trim(), DEBUG_MESSAGE_LENGTH)}`;
const levelStr = typeof level === 'string' ? level : String(level);
const isErrorOrWarn = levelStr.includes('error') || levelStr.includes('warn');
const finalize = (text) => (isErrorOrWarn ? redactMessage(text) : text);
try {
const levelStr = typeof level === 'string' ? level : String(level);
const isErrorOrWarn = levelStr.includes('error') || levelStr.includes('warn');
if (level !== 'debug' && !isErrorOrWarn) {
return msg;
return finalize(msg);
}
if (!metadata) {
return msg;
return finalize(msg);
}
const debugValue = metadata[SPLAT_SYMBOL]?.[0] ?? extractMetaObject(metadata);
if (!debugValue) {
return msg;
return finalize(msg);
}
if (debugValue && Array.isArray(debugValue)) {
msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`;
return msg;
return finalize(msg);
}
if (typeof debugValue !== 'object') {
return (msg += ` ${debugValue}`);
return finalize((msg += ` ${debugValue}`));
}
msg += '\n{';
@ -228,9 +229,9 @@ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...met
});
msg += '\n}';
return msg;
return finalize(msg);
} catch (e) {
return (msg += `\n[LOGGER PARSING ERROR] ${e.message}`);
return finalize((msg += `\n[LOGGER PARSING ERROR] ${e.message}`));
}
});