From 1e43d636877cd44ec4df8bc46a546c42eb1995ee Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 11:40:38 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20fix:=20Redact=20Before=20Coloriz?= =?UTF-8?q?e=20To=20Survive=20ANSI=20Word-Boundary=20Interference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console pipeline runs `redactFormat → colorize({ all: true }) → printf`. With `all: true`, winston wraps `info.message` in ANSI escapes whose trailing `m` is a word character. That means `\b(Bearer )…` placed at the start of a colorized segment can fall on a (word,word) boundary and miss — the earlier line-wise `redactMessage(line)` pass in printf suffers the same issue because it runs after colorize. Extend `redactFormat` to run for `warn` in addition to `error`, operating on the raw pre-colorize `info.message` + `Symbol.for('message')` strings. The later in-printf `redactMessage(line)` stays as a backstop, but the primary redaction now happens where the regex can actually see the text. Metadata redaction already operates on the raw info object via `formatConsoleMeta`, so it was never affected by ANSI — no change there. Includes regression tests for the new warn-level behavior and for the info/debug no-op path. Reviewed-by: Codex (P2 finding on PR #12737, commit fdb6b361). --- api/config/__tests__/parsers.spec.js | 25 ++++++++++++++++++++++++- api/config/parsers.js | 15 +++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/api/config/__tests__/parsers.spec.js b/api/config/__tests__/parsers.spec.js index 5f1391ff2a..db2a0dea65 100644 --- a/api/config/__tests__/parsers.spec.js +++ b/api/config/__tests__/parsers.spec.js @@ -1,6 +1,7 @@ jest.unmock('winston'); -const { formatConsoleMeta, redactMessage, debugTraverse } = jest.requireActual('../parsers'); +const { formatConsoleMeta, redactMessage, redactFormat, debugTraverse } = + jest.requireActual('../parsers'); const SPLAT_SYMBOL = Symbol.for('splat'); describe('formatConsoleMeta', () => { @@ -153,6 +154,28 @@ describe('redactMessage', () => { }); }); +describe('redactFormat', () => { + const runFormat = (info) => redactFormat().transform(info) || info; + + it('redacts info.message for error level before any colorize step runs', () => { + const info = runFormat({ level: 'error', message: 'Bearer secretvalue' }); + expect(info.message).toBe('Bearer [REDACTED]'); + }); + + it('redacts info.message for warn level too (avoids ANSI boundary issues later)', () => { + const info = runFormat({ level: 'warn', message: 'apiKey=sk-abc123def' }); + expect(info.message).toContain('sk-[REDACTED]'); + }); + + it('leaves info.message untouched for info and debug levels', () => { + const infoInfo = runFormat({ level: 'info', message: 'Bearer looksSensitive' }); + expect(infoInfo.message).toBe('Bearer looksSensitive'); + + const infoDebug = runFormat({ level: 'debug', message: 'Bearer looksSensitive' }); + expect(infoDebug.message).toBe('Bearer looksSensitive'); + }); +}); + describe('debugTraverse', () => { const runFormatter = (info) => { const transformed = debugTraverse.transform(info); diff --git a/api/config/parsers.js b/api/config/parsers.js index 51fa4b4e1e..29caf79bfe 100644 --- a/api/config/parsers.js +++ b/api/config/parsers.js @@ -41,15 +41,22 @@ function redactMessage(str, trimLength) { } /** - * Redacts sensitive information from log messages if the log level is 'error'. + * Redacts sensitive information from log messages when the log level is + * `error` or `warn`. Runs on the raw `info.message` before any colorize / + * splat transforms so the sensitive-token regexes don't have to contend + * with ANSI escape sequences (whose trailing `m` would otherwise defeat + * `\b` anchors). + * * Note: Intentionally mutates the object. * @param {Object} info - The log information object. * @returns {Object} - The modified log information object. */ const redactFormat = winston.format((info) => { - if (info.level === 'error') { - info.message = redactMessage(info.message); - if (info[MESSAGE_SYMBOL]) { + if (info.level === 'error' || info.level === 'warn') { + if (typeof info.message === 'string') { + info.message = redactMessage(info.message); + } + if (typeof info[MESSAGE_SYMBOL] === 'string') { info[MESSAGE_SYMBOL] = redactMessage(info[MESSAGE_SYMBOL]); } }