fix(gateway): redact secrets in skills.update response

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
This commit is contained in:
Ziy1-Tan
2026-04-22 13:44:32 +08:00
committed by Mason Huang
parent d76f924be3
commit 3cf89613f1
2 changed files with 52 additions and 1 deletions

View File

@@ -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,
);
},
};

View File

@@ -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<string, unknown> }).config;
expect(config.apiKey).not.toBe("secret-api-key-123");
const env = config.env as Record<string, string>;
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");
});
});