fix(logging): redact secrets at subsystem console sink (#73284)

createSubsystemLogger writes through writeConsoleLine, which intentionally
bypasses the patched console.* capture handler in src/logging/console.ts to
avoid recursion. That bypass also skipped the sink-boundary
redactSensitiveText() gate, so secrets reaching subsystem loggers as
message strings or formatted meta could appear verbatim on the terminal —
a follow-up to the file-transport redaction landed in #67953, tracked
under #64046.

Apply redactSensitiveText() at the writeConsoleLine() exit, immediately
after the existing Windows surrogate sanitization and before dispatching
to the rawConsole sink. This covers all subsystem console paths
(trace/debug/info/warn/error/fatal and .raw) because they share the same
writeConsoleLine() exit, matching the redact-at-sink-boundary pattern
already used in console.ts and the file transport.

Closes #73284
This commit is contained in:
edwin-rivera-dev
2026-04-28 07:44:31 +02:00
committed by Peter Steinberger
parent 3c636208b0
commit f2df49ab4b
3 changed files with 39 additions and 3 deletions

View File

@@ -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<TOKEN>` 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

View File

@@ -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");

View File

@@ -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<string, unknown>;
@@ -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);
}
}