Skip to content

Commit a853c5e

Browse files
koshajisallyom
andauthored
fix(config-audit): redact CLI argv secrets before persisting to log (#75095)
Merged via squash. Prepared head SHA: 3dc54de Co-authored-by: koshaji <[email protected]> Co-authored-by: sallyom <[email protected]> Reviewed-by: @sallyom
1 parent e7dafaf commit a853c5e

5 files changed

Lines changed: 368 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313

1414
- Agents/commitments: keep inferred follow-ups internal when heartbeat target is none, strip raw source text from stored commitments, disable tools during due-commitment heartbeat turns, bound hidden extraction queue growth, expire stale commitments, and add QA/Docker safety coverage. Thanks @vignesh07.
1515
- Agents/commitments: run hidden follow-up extraction on the configured agent/default model instead of falling back to direct OpenAI, so OpenAI Codex OAuth-only gateways no longer spam background API-key failures. Fixes #75334. Thanks @sene1337.
16+
- Security/config-audit: redact CLI argv and execArgv secrets before persisting config audit records, covering write, observe, and recovery paths. Fixes #60826. Thanks @koshaji.
1617
- Plugins/runtime-deps: accept already materialized package-level runtime-deps supersets as converged, so later lazy plugin activation no longer prunes and relaunches `pnpm install` after gateway startup pre-staging, reducing event-loop pressure from repeated runtime-deps repair on packaged installs. Fixes #75283; refs #75297 and #72338. Thanks @brokemac79, @lisandromachado, and @midhunmonachan.
1718
- Discord: retry queued REST 429s against learned bucket/global cooldowns and reacquire fresh voice upload URLs after CDN upload rate limits, so outbound sends recover without reusing stale single-use upload URLs. Thanks @discord.
1819
- TTS/providers: keep bundled speech-provider compat fallback available when plugins are globally disabled, so cold gateway and CLI startup can still resolve fallback speech providers instead of leaving explicit TTS provider selection with no registered providers. Refs #75265. Thanks @sliekens.

src/config/io.audit.test.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
createConfigWriteAuditRecordBase,
88
finalizeConfigWriteAuditRecord,
99
formatConfigOverwriteLogMessage,
10+
redactConfigAuditArgv,
1011
resolveConfigAuditLogPath,
1112
} from "./io.audit.js";
1213

@@ -195,6 +196,231 @@ describe("config io audit helpers", () => {
195196
});
196197
});
197198

199+
it("redacts argv values that follow known secret flag names", () => {
200+
const argv = [
201+
"node",
202+
"openclaw",
203+
"gateway",
204+
"--token",
205+
"super-secret-gateway-token-12345",
206+
"--api-key",
207+
"sk-very-real-looking-openai-api-key-AB12CD34",
208+
"--port",
209+
"8080",
210+
];
211+
const result = redactConfigAuditArgv(argv);
212+
expect(result).toEqual([
213+
"node",
214+
"openclaw",
215+
"gateway",
216+
"--token",
217+
"***",
218+
"--api-key",
219+
"***",
220+
"--port",
221+
"8080",
222+
]);
223+
});
224+
225+
it("redacts the value half of `--flag=value` for secret flags", () => {
226+
const argv = ["openclaw", "--token=ghp_realgithubtoken1234567890ABCD", "--port=8080"];
227+
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token=***", "--port=8080"]);
228+
});
229+
230+
it("redacts standalone token shapes via the shared logging redaction patterns", () => {
231+
const argv = [
232+
"node",
233+
"openclaw",
234+
"ghp_realgithubtoken1234567890ABCD",
235+
"AIzaSyD-very-real-looking-google-api-key-123",
236+
"987654321:AAAAAAAAAAAAAAAAAAAAAAAAAAAA",
237+
];
238+
const result = redactConfigAuditArgv(argv);
239+
expect(result[0]).toBe("node");
240+
expect(result[1]).toBe("openclaw");
241+
for (const masked of result.slice(2)) {
242+
expect(masked).not.toContain("ghp_realgithubtoken");
243+
expect(masked).not.toContain("AIzaSyD-very-real-looking");
244+
expect(masked).not.toMatch(/AAAAAAAAAAAAAA/);
245+
}
246+
});
247+
248+
it("leaves non-secret arguments untouched", () => {
249+
const argv = ["node", "openclaw", "gateway", "--port", "8080", "--bind", "lan"];
250+
expect(redactConfigAuditArgv(argv)).toEqual(argv);
251+
});
252+
253+
it("redacts unknown but credential-suffixed flags via the heuristic classifier", () => {
254+
const argv = [
255+
"node",
256+
"openclaw",
257+
"--custom-api-key",
258+
"real-tenant-key-AB12CD34EF56GH78",
259+
"--alibaba-model-studio-api-key=plain-value-xyz-12345",
260+
"--app-token",
261+
"another-secret-value",
262+
"--frobnicate-credential=hidden",
263+
];
264+
const result = redactConfigAuditArgv(argv);
265+
expect(result).toEqual([
266+
"node",
267+
"openclaw",
268+
"--custom-api-key",
269+
"***",
270+
"--alibaba-model-studio-api-key=***",
271+
"--app-token",
272+
"***",
273+
"--frobnicate-credential=***",
274+
]);
275+
});
276+
277+
it("redacts key-valued secret flags (Nostr --private-key, Matrix --recovery-key)", () => {
278+
const argv = [
279+
"node",
280+
"openclaw",
281+
"channels",
282+
"add",
283+
"--channel",
284+
"nostr",
285+
"--private-key",
286+
"nsec1realnostrprivatekeyvaluexyz1234567890",
287+
"--recovery-key=EsTb-ABCD-1234-EFGH-5678-IJKL-9012-MNOP",
288+
];
289+
const result = redactConfigAuditArgv(argv);
290+
expect(result).toEqual([
291+
"node",
292+
"openclaw",
293+
"channels",
294+
"add",
295+
"--channel",
296+
"nostr",
297+
"--private-key",
298+
"***",
299+
"--recovery-key=***",
300+
]);
301+
});
302+
303+
it("redacts unknown *-key flags via the heuristic classifier (private/signing/master/etc.)", () => {
304+
const argv = [
305+
"node",
306+
"openclaw",
307+
"--my-plugin-private-key",
308+
"tenant-private-key-material-zzz",
309+
"--rotated-signing-key=PEM-LIKE-MATERIAL",
310+
"--ops-master-key",
311+
"ABCDEF1234567890",
312+
];
313+
const result = redactConfigAuditArgv(argv);
314+
expect(result).toEqual([
315+
"node",
316+
"openclaw",
317+
"--my-plugin-private-key",
318+
"***",
319+
"--rotated-signing-key=***",
320+
"--ops-master-key",
321+
"***",
322+
]);
323+
});
324+
325+
it("masks the next arg after a secret flag even when it looks like another option", () => {
326+
const argv = ["openclaw", "--token", "--port", "8080"];
327+
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token", "***", "8080"]);
328+
});
329+
330+
it("redacts dash-leading secret values after bare secret flags", () => {
331+
const argv = ["openclaw", "--password", "-secret-value"];
332+
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--password", "***"]);
333+
});
334+
335+
it("does not mask when a secret flag is the final arg with no value", () => {
336+
const argv = ["openclaw", "--token"];
337+
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token"]);
338+
});
339+
340+
it("caps caller-supplied processInfo argv at 8 entries before redaction", () => {
341+
const longArgv = [
342+
"node",
343+
"openclaw",
344+
"--api-key",
345+
"secret",
346+
"--port",
347+
"8080",
348+
"--bind",
349+
"lan",
350+
"--leaks-here-token",
351+
"this-must-not-land-in-audit-1234567890",
352+
];
353+
const base = createConfigWriteAuditRecordBase({
354+
configPath: "/tmp/openclaw.json",
355+
env: {} as NodeJS.ProcessEnv,
356+
existsBefore: true,
357+
previousHash: "prev",
358+
nextHash: "next",
359+
previousBytes: 1,
360+
nextBytes: 2,
361+
previousMetadata: {
362+
dev: null,
363+
ino: null,
364+
mode: null,
365+
nlink: null,
366+
uid: null,
367+
gid: null,
368+
},
369+
changedPathCount: 0,
370+
hasMetaBefore: true,
371+
hasMetaAfter: true,
372+
gatewayModeBefore: "local",
373+
gatewayModeAfter: "local",
374+
suspicious: [],
375+
now: "2026-04-30T00:00:00.000Z",
376+
processInfo: {
377+
pid: 1,
378+
ppid: 1,
379+
cwd: "/work",
380+
argv: longArgv,
381+
execArgv: [],
382+
},
383+
});
384+
expect(base.argv).toHaveLength(8);
385+
expect(base.argv).not.toContain("this-must-not-land-in-audit-1234567890");
386+
expect(base.argv).not.toContain("--leaks-here-token");
387+
});
388+
389+
it("redacts processInfo.argv when explicitly supplied to createConfigWriteAuditRecordBase", () => {
390+
const base = createConfigWriteAuditRecordBase({
391+
configPath: "/tmp/openclaw.json",
392+
env: {} as NodeJS.ProcessEnv,
393+
existsBefore: true,
394+
previousHash: "prev",
395+
nextHash: "next",
396+
previousBytes: 1,
397+
nextBytes: 2,
398+
previousMetadata: {
399+
dev: null,
400+
ino: null,
401+
mode: null,
402+
nlink: null,
403+
uid: null,
404+
gid: null,
405+
},
406+
changedPathCount: 0,
407+
hasMetaBefore: true,
408+
hasMetaAfter: true,
409+
gatewayModeBefore: "local",
410+
gatewayModeAfter: "local",
411+
suspicious: [],
412+
now: "2026-04-30T00:00:00.000Z",
413+
processInfo: {
414+
pid: 1,
415+
ppid: 1,
416+
cwd: "/work",
417+
argv: ["node", "openclaw", "--token", "leaked-but-not-anymore-12345"],
418+
execArgv: [],
419+
},
420+
});
421+
expect(base.argv).toEqual(["node", "openclaw", "--token", "***"]);
422+
});
423+
198424
it("also accepts flattened audit record params from legacy call sites", async () => {
199425
const home = await suiteRootTracker.make("append-flat");
200426
const record = createRenameAuditRecord(home);

0 commit comments

Comments
 (0)