diff --git a/CHANGELOG.md b/CHANGELOG.md index 71377eb5bc4..d68fa896b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Feishu/inbound files: recover CJK filenames from plain `Content-Disposition: filename=` download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing. - Channels/Telegram: normalize accidental full `/bot` Telegram `apiRoot` values at runtime and teach `openclaw doctor --fix` to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris. - Zalo Personal: persist refreshed `zca-js` session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa. +- Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so `createSubsystemLogger().info/warn/error` output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev. ## 2026.4.27 diff --git a/src/logging/subsystem.test.ts b/src/logging/subsystem.test.ts index b53c72da23f..e5a7052e0f6 100644 --- a/src/logging/subsystem.test.ts +++ b/src/logging/subsystem.test.ts @@ -205,6 +205,34 @@ describe("createSubsystemLogger().isEnabled", () => { expect(warn).toHaveBeenCalledTimes(1); }); + it("redacts sensitive tokens at the console sink so subsystem writes do not leak secrets (#73284)", () => { + setLoggerOverride({ level: "silent", consoleLevel: "warn" }); + const warn = installConsoleMethodSpy("warn"); + const log = createSubsystemLogger("gateway"); + const secret = "sk-supersecretvaluefortest12345"; + + log.warn(`token=${secret}`); + + expect(warn).toHaveBeenCalledTimes(1); + const written = String(warn.mock.calls[0]?.[0] ?? ""); + expect(written).not.toContain(secret); + expect(written).toMatch(/sk-sup…2345|\*\*\*/); + }); + + it("redacts Bearer tokens on subsystem error console writes", () => { + setLoggerOverride({ level: "silent", consoleLevel: "error" }); + const error = installConsoleMethodSpy("error"); + const log = createSubsystemLogger("gateway").child("auth"); + const bearer = "Bearer abcdefghijklmnopqrstuvwxyz"; + + log.error(`Authorization failed: ${bearer}`); + + expect(error).toHaveBeenCalledTimes(1); + const written = String(error.mock.calls[0]?.[0] ?? ""); + expect(written).not.toContain("abcdefghijklmnopqrstuvwxyz"); + expect(written).toContain("Bearer "); + }); + it("keeps long-lived subsystem loggers on the current-day rolling file", () => { const logDir = path.dirname(logPathTracker.nextPath()); const firstDay = path.join(logDir, "openclaw-2026-01-01.log"); diff --git a/src/logging/subsystem.ts b/src/logging/subsystem.ts index e8acab0f432..d6b581b991b 100644 --- a/src/logging/subsystem.ts +++ b/src/logging/subsystem.ts @@ -12,6 +12,7 @@ import { } from "./console.js"; import { type LogLevel, levelToMinLevel } from "./levels.js"; import { getChildLogger, isFileLogLevelEnabled } from "./logger.js"; +import { redactSensitiveText } from "./redact.js"; import { loggingState } from "./state.js"; type LogObj = { date?: Date } & Record; @@ -255,13 +256,19 @@ function writeConsoleLine(level: LogLevel, line: string) { process.platform === "win32" && process.env.GITHUB_ACTIONS === "true" ? line.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "?").replace(/[\uD800-\uDFFF]/g, "?") : line; + // Subsystem console output bypasses the patched console.* capture handler in + // ./console.ts to avoid recursion, so the sink-boundary redaction applied + // there does not run for these writes (#73284). Redact at this exit instead + // so secrets reaching subsystem loggers as message strings or formatted meta + // do not appear verbatim on the terminal. + const redacted = redactSensitiveText(sanitized); const sink = loggingState.rawConsole ?? console; if (loggingState.forceConsoleToStderr || level === "error" || level === "fatal") { - (sink.error ?? console.error)(sanitized); + (sink.error ?? console.error)(redacted); } else if (level === "warn") { - (sink.warn ?? console.warn)(sanitized); + (sink.warn ?? console.warn)(redacted); } else { - (sink.log ?? console.log)(sanitized); + (sink.log ?? console.log)(redacted); } }