fix(security): redact secrets in exec approval prompts (#61077) (#64790)

Merged via squash.

Prepared head SHA: 324202d37e
Co-authored-by: feiskyer <676637+feiskyer@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Pengfei Ni
2026-04-15 18:02:10 +08:00
committed by GitHub
parent dcaccdc5c4
commit e99a24d645
3 changed files with 27 additions and 1 deletions

View File

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

View File

@@ -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", () => {

View File

@@ -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 {