From 3cf89613f11451b82442fd7e40e564bedc07ba64 Mon Sep 17 00:00:00 2001 From: Ziy1-Tan Date: Wed, 22 Apr 2026 13:44:32 +0800 Subject: [PATCH] fix(gateway): redact secrets in skills.update response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skills.update was returning the full updated SkillConfig entry including plaintext apiKey and env values in the RPC response. This could leak credentials into Control UI WebSocket traffic, client logs, or session transcripts. Fix by passing the response config through the existing redactConfigObject() utility, which already handles all sensitive paths (apiKey, tokens, passwords, secret-named env keys, etc.) via isSensitiveConfigPath(). Config is still written to disk in full — only the response payload is redacted. Fixes #66769 --- src/gateway/server-methods/skills.ts | 7 ++- .../skills.update.normalizes-api-key.test.ts | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/gateway/server-methods/skills.ts b/src/gateway/server-methods/skills.ts index 4fd8994098d..f3422bd6ce8 100644 --- a/src/gateway/server-methods/skills.ts +++ b/src/gateway/server-methods/skills.ts @@ -14,6 +14,7 @@ import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js"; import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js"; import { listAgentWorkspaceDirs } from "../../agents/workspace-dirs.js"; import { loadConfig, writeConfigFile } from "../../config/config.js"; +import { redactConfigObject } from "../../config/redact-snapshot.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { fetchClawHubSkillDetail } from "../../infra/clawhub.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -341,6 +342,10 @@ export const skillsHandlers: GatewayRequestHandlers = { skills, }; await writeConfigFile(nextConfig); - respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined); + respond( + true, + { ok: true, skillKey: p.skillKey, config: redactConfigObject(current) }, + undefined, + ); }, }; diff --git a/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts b/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts index 6476ad671d0..fe4af5f507f 100644 --- a/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts +++ b/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts @@ -50,4 +50,50 @@ describe("skills.update", () => { }, }); }); + + it("redacts apiKey and secret env values from the response but writes full values to config", async () => { + writtenConfig = null; + + let responseResult: unknown = null; + await skillsHandlers["skills.update"]({ + params: { + skillKey: "demo-skill", + apiKey: "secret-api-key-123", + env: { + GEMINI_API_KEY: "secret-env-key-456", + BRAVE_REGION: "us", + }, + }, + req: {} as never, + client: null as never, + isWebchatConnect: () => false, + context: {} as never, + respond: (_success, result, _err) => { + responseResult = result; + }, + }); + + // Full values must be persisted to config + expect(writtenConfig).toMatchObject({ + skills: { + entries: { + "demo-skill": { + apiKey: "secret-api-key-123", + env: { + GEMINI_API_KEY: "secret-env-key-456", + BRAVE_REGION: "us", + }, + }, + }, + }, + }); + + // Response must not expose plaintext secrets + const config = (responseResult as { config: Record }).config; + expect(config.apiKey).not.toBe("secret-api-key-123"); + const env = config.env as Record; + expect(env.GEMINI_API_KEY).not.toBe("secret-env-key-456"); + // Non-secret env values should still be present + expect(env.BRAVE_REGION).toBe("us"); + }); });