diff --git a/CHANGELOG.md b/CHANGELOG.md index d3865e9b4d6..2d6afc6facf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -248,6 +248,7 @@ Docs: https://docs.openclaw.ai - MiniMax/OAuth: write `api: "anthropic-messages"` and `authHeader: true` into the `minimax-portal` config patch during `openclaw configure`, so re-authenticated portal setups keep Bearer auth routing working. (#64964) Thanks @ryanlee666. - Agents/tools: stop repeated unavailable-tool retries from escaping loop detection when the model changes arguments, and rewrite over-threshold unknown tool calls into plain assistant text before dispatch. (#65922) Thanks @dutifulbob. - Cron/announce delivery: tell isolated cron jobs to return the full response exactly instead of a summary, so structured `--announce` deliveries stop dropping fields nondeterministically. (#65638) Thanks @srinivaspavan9 and @vincentkoc. +- Security/exec approvals: redact bearer tokens, API keys, and similar secrets in exec approval prompt command text before those prompts are posted back to chat channels, regardless of logging redaction settings. (#61077) Thanks @feiskyer and @vincentkoc. ## 2026.4.10 diff --git a/src/infra/exec-approval-command-display.test.ts b/src/infra/exec-approval-command-display.test.ts index df8f0edf131..a69a63b94b5 100644 --- a/src/infra/exec-approval-command-display.test.ts +++ b/src/infra/exec-approval-command-display.test.ts @@ -11,6 +11,29 @@ describe("sanitizeExecApprovalDisplayText", () => { ])("sanitizes exec approval display text for %j", (input, expected) => { expect(sanitizeExecApprovalDisplayText(input)).toBe(expected); }); + + it("redacts bearer tokens embedded in commands", () => { + const cmd = + 'curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig" https://api.example.com'; + const result = sanitizeExecApprovalDisplayText(cmd); + expect(result).not.toContain("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.sig"); + expect(result).toContain("curl"); + expect(result).toContain("https://api.example.com"); + }); + + it("redacts API keys in environment variable assignments", () => { + const cmd = 'API_SECRET="sk-abc123456789012345678" python script.py'; + const result = sanitizeExecApprovalDisplayText(cmd); + expect(result).not.toContain("sk-abc123456789012345678"); + expect(result).toContain("python script.py"); + }); + + it("redacts GitHub personal access tokens", () => { + const cmd = "git clone https://ghp_1234567890abcdefghij1234567890abcdef@github.com/user/repo"; + const result = sanitizeExecApprovalDisplayText(cmd); + expect(result).not.toContain("ghp_1234567890abcdefghij1234567890abcdef"); + expect(result).toContain("git clone"); + }); }); describe("resolveExecApprovalCommandDisplay", () => { diff --git a/src/infra/exec-approval-command-display.ts b/src/infra/exec-approval-command-display.ts index 318968b5659..273f0fd4c33 100644 --- a/src/infra/exec-approval-command-display.ts +++ b/src/infra/exec-approval-command-display.ts @@ -1,3 +1,4 @@ +import { redactSensitiveText } from "../logging/redact.js"; import type { ExecApprovalRequestPayload } from "./exec-approvals.js"; // Escape invisible characters that can spoof approval prompts in common UIs. @@ -8,7 +9,8 @@ function formatCodePointEscape(char: string): string { } export function sanitizeExecApprovalDisplayText(commandText: string): string { - return commandText.replace(EXEC_APPROVAL_INVISIBLE_CHAR_REGEX, formatCodePointEscape); + const escaped = commandText.replace(EXEC_APPROVAL_INVISIBLE_CHAR_REGEX, formatCodePointEscape); + return redactSensitiveText(escaped, { mode: "tools" }); } function normalizePreview(commandText: string, commandPreview?: string | null): string | null {