Skip to content

Commit 4cba24a

Browse files
committed
fix(logging): redact console and file sinks
1 parent 1a8f765 commit 4cba24a

6 files changed

Lines changed: 108 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ Docs: https://docs.openclaw.ai
7171

7272
- Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd.
7373
- Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.
74+
- Logging: redact configured secret patterns at console and file-log sink exits
75+
so credentials that reach the logger are masked before terminal display or
76+
JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.
7477
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
7578
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
7679
start browser-capable CLI node services through the restored

docs/logging.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ Tool summaries can redact sensitive tokens before they hit the console:
167167
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
168168
- `logging.redactPatterns`: list of regex strings to override the default set
169169

170-
Redaction affects **console output only** and does not alter file logs.
170+
Redaction applies at the logging sinks for **console output**, **stderr-routed
171+
console diagnostics**, and **file logs**. File logs stay JSONL, but matching
172+
secret values are masked before the line is written to disk.
171173

172174
## Diagnostics and OpenTelemetry
173175

src/logging/console-capture.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ afterAll(async () => {
4747
});
4848

4949
describe("enableConsoleCapture", () => {
50+
const secret = "sk-testsecret1234567890abcd";
51+
5052
it("swallows EIO from stderr writes", () => {
5153
setLoggerOverride({ level: "info", file: tempLogPath() });
5254
vi.spyOn(process.stderr, "write").mockImplementation(() => {
@@ -123,6 +125,50 @@ describe("enableConsoleCapture", () => {
123125
expect(stdoutWrite).toHaveBeenCalledWith('{\n "ok": true\n}\n');
124126
});
125127

128+
it("redacts credentials before forwarding console output", () => {
129+
setLoggerOverride({ level: "info", file: tempLogPath() });
130+
const log = vi.fn();
131+
console.log = log;
132+
enableConsoleCapture();
133+
134+
console.log("apiKey:", secret);
135+
136+
expect(log).toHaveBeenCalledTimes(1);
137+
const line = String(log.mock.calls[0]?.[0] ?? "");
138+
expect(line).toContain("apiKey:");
139+
expect(line).not.toContain(secret);
140+
});
141+
142+
it("redacts credentials before writing forced stderr console output", () => {
143+
setLoggerOverride({ level: "info", file: tempLogPath() });
144+
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
145+
routeLogsToStderr();
146+
enableConsoleCapture();
147+
148+
console.error(`Authorization: Bearer ${secret}`);
149+
150+
expect(stderrWrite).toHaveBeenCalledTimes(1);
151+
const line = String(stderrWrite.mock.calls[0]?.[0] ?? "");
152+
expect(line).toContain("Authorization: Bearer");
153+
expect(line).not.toContain(secret);
154+
});
155+
156+
it("redacts credentials when timestamp prefixing console output", () => {
157+
setLoggerOverride({ level: "info", file: tempLogPath() });
158+
const warn = vi.fn();
159+
console.warn = warn;
160+
setConsoleTimestampPrefix(true);
161+
enableConsoleCapture();
162+
163+
console.warn(`token=${secret}`);
164+
165+
expect(warn).toHaveBeenCalledTimes(1);
166+
const line = String(warn.mock.calls[0]?.[0] ?? "");
167+
expect(line).toMatch(/^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T)/);
168+
expect(line).toContain("token=");
169+
expect(line).not.toContain(secret);
170+
});
171+
126172
it.each([
127173
{ name: "stdout", stream: process.stdout },
128174
{ name: "stderr", stream: process.stderr },

src/logging/console.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { resolveEnvLogLevelOverride } from "./env-log-level.js";
77
import { type LogLevel, normalizeLogLevel } from "./levels.js";
88
import { getLogger } from "./logger.js";
99
import { resolveNodeRequireFromMeta } from "./node-require.js";
10+
import { redactSensitiveText } from "./redact.js";
1011
import { loggingState } from "./state.js";
1112
import { formatLocalIsoWithOffset, formatTimestamp } from "./timestamps.js";
1213
import type { ConsoleStyle, LoggerSettings } from "./types.js";
@@ -275,7 +276,8 @@ export function enableConsoleCapture(): void {
275276
if (loggingState.forceConsoleToStderr) {
276277
// In --json mode, all console.* writes are diagnostics and should stay off stdout.
277278
try {
278-
const line = timestamp ? `${timestamp} ${formatted}` : formatted;
279+
const redacted = redactSensitiveText(formatted);
280+
const line = timestamp ? `${timestamp} ${redacted}` : redacted;
279281
process.stderr.write(`${line}\n`);
280282
} catch (err) {
281283
if (isEpipeError(err)) {
@@ -285,19 +287,16 @@ export function enableConsoleCapture(): void {
285287
}
286288
} else {
287289
try {
290+
const redacted = redactSensitiveText(formatted);
288291
if (!timestamp) {
289-
orig.apply(console, args as []);
292+
if (args.length === 0) {
293+
orig.apply(console, args as []);
294+
return;
295+
}
296+
orig.call(console, redacted);
290297
return;
291298
}
292-
if (args.length === 0) {
293-
orig.call(console, timestamp);
294-
return;
295-
}
296-
if (typeof args[0] === "string") {
297-
orig.call(console, `${timestamp} ${args[0]}`, ...args.slice(1));
298-
return;
299-
}
300-
orig.call(console, timestamp, ...args);
299+
orig.call(console, redacted ? `${timestamp} ${redacted}` : timestamp);
301300
} catch (err) {
302301
if (isEpipeError(err)) {
303302
return;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import fs from "node:fs";
2+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
3+
import { getLogger, resetLogger, setLoggerOverride } from "../logging.js";
4+
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
5+
6+
const secret = "sk-testsecret1234567890abcd";
7+
const logPathTracker = createSuiteLogPathTracker("openclaw-log-redaction-");
8+
9+
beforeAll(async () => {
10+
await logPathTracker.setup();
11+
});
12+
13+
afterEach(() => {
14+
resetLogger();
15+
setLoggerOverride(null);
16+
});
17+
18+
afterAll(async () => {
19+
await logPathTracker.cleanup();
20+
});
21+
22+
describe("file log redaction", () => {
23+
it("redacts credential fields before writing JSONL file logs", () => {
24+
const logPath = logPathTracker.nextPath();
25+
setLoggerOverride({ level: "info", file: logPath });
26+
27+
getLogger().info({ apiKey: secret, message: "provider configured" });
28+
29+
const content = fs.readFileSync(logPath, "utf8");
30+
expect(content).toContain("provider configured");
31+
expect(content).toContain('"apiKey"');
32+
expect(content).not.toContain(secret);
33+
});
34+
35+
it("redacts bearer tokens in file log message strings", () => {
36+
const logPath = logPathTracker.nextPath();
37+
setLoggerOverride({ level: "info", file: logPath });
38+
39+
getLogger().warn({ message: `Authorization: Bearer ${secret}` });
40+
41+
const content = fs.readFileSync(logPath, "utf8");
42+
expect(content).toContain("Authorization: Bearer");
43+
expect(content).not.toContain(secret);
44+
});
45+
});

src/logging/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
423423
logger.attachTransport((logObj: LogObj) => {
424424
try {
425425
const time = formatTimestamp(logObj.date ?? new Date(), { style: "long" });
426-
const line = JSON.stringify({ ...logObj, time });
426+
const line = redactSensitiveText(JSON.stringify({ ...logObj, time }));
427427
const payload = `${line}\n`;
428428
const payloadBytes = Buffer.byteLength(payload, "utf8");
429429
const nextBytes = currentFileBytes + payloadBytes;

0 commit comments

Comments
 (0)