From e8cb0484ce6c0e43a8e7ce3a6fdfba4c04c4be54 Mon Sep 17 00:00:00 2001 From: Cui Chen Date: Wed, 4 Mar 2026 07:11:49 +0800 Subject: [PATCH] fix(security): strip partial API token from status labels (#33262) Merged via squash. Prepared head SHA: 5fe81704e678dc5b18ca9416e5eb0750cfe49fb6 Co-authored-by: cu1ch3n <80438676+cu1ch3n@users.noreply.github.com> Co-authored-by: grp06 <1573959+grp06@users.noreply.github.com> Reviewed-by: @grp06 --- CHANGELOG.md | 1 + src/agents/model-auth-label.test.ts | 35 ++++++++++++++++++++++++----- src/agents/model-auth-label.ts | 35 ++++------------------------- src/plugins/commands.test.ts | 1 + 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd065ff2ce..f595a666416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n. - Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan. - Docs/security threat-model links: replace relative `.md` links with Mintlify-compatible root-relative routes in security docs to prevent broken internal navigation. (#27698) thanks @clawdoo. - iOS/Voice timing safety: guard system speech start/finish callbacks to the active utterance to avoid misattributed start events during rapid stop/restart cycles. (#33304) thanks @mbelinky; original implementation direction by @ngutman. diff --git a/src/agents/model-auth-label.test.ts b/src/agents/model-auth-label.test.ts index adcb6ce49b6..85fa4bc43fb 100644 --- a/src/agents/model-auth-label.test.ts +++ b/src/agents/model-auth-label.test.ts @@ -25,13 +25,14 @@ describe("resolveModelAuthLabel", () => { resolveAuthProfileDisplayLabelMock.mockReset(); }); - it("does not throw when token profile only has tokenRef", () => { + it("does not include token value in label for token profiles", () => { ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: { "github-copilot:default": { type: "token", provider: "github-copilot", + token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" }, }, }, @@ -45,10 +46,12 @@ describe("resolveModelAuthLabel", () => { sessionEntry: { authProfileOverride: "github-copilot:default" } as never, }); - expect(label).toContain("token ref(env:GITHUB_TOKEN)"); + expect(label).toBe("token (github-copilot:default)"); + expect(label).not.toContain("ghp_"); + expect(label).not.toContain("ref("); }); - it("masks short api-key profile values", () => { + it("does not include api-key value in label for api-key profiles", () => { const shortSecret = "abc123"; ensureAuthProfileStoreMock.mockReturnValue({ version: 1, @@ -69,8 +72,30 @@ describe("resolveModelAuthLabel", () => { sessionEntry: { authProfileOverride: "openai:default" } as never, }); - expect(label).toContain("api-key"); - expect(label).toContain("..."); + expect(label).toBe("api-key (openai:default)"); expect(label).not.toContain(shortSecret); + expect(label).not.toContain("..."); + }); + + it("shows oauth type with profile label", () => { + ensureAuthProfileStoreMock.mockReturnValue({ + version: 1, + profiles: { + "anthropic:oauth": { + type: "oauth", + provider: "anthropic", + }, + }, + } as never); + resolveAuthProfileOrderMock.mockReturnValue(["anthropic:oauth"]); + resolveAuthProfileDisplayLabelMock.mockReturnValue("anthropic:oauth"); + + const label = resolveModelAuthLabel({ + provider: "anthropic", + cfg: {}, + sessionEntry: { authProfileOverride: "anthropic:oauth" } as never, + }); + + expect(label).toBe("oauth (anthropic:oauth)"); }); }); diff --git a/src/agents/model-auth-label.ts b/src/agents/model-auth-label.ts index 4538cc1c872..ca564ab4dec 100644 --- a/src/agents/model-auth-label.ts +++ b/src/agents/model-auth-label.ts @@ -1,6 +1,5 @@ import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; -import { maskApiKey } from "../utils/mask-api-key.js"; import { ensureAuthProfileStore, resolveAuthProfileDisplayLabel, @@ -9,28 +8,6 @@ import { import { getCustomProviderApiKey, resolveEnvApiKey } from "./model-auth.js"; import { normalizeProviderId } from "./model-selection.js"; -function formatApiKeySnippet(apiKey: string): string { - const compact = apiKey.replace(/\s+/g, ""); - if (!compact) { - return "unknown"; - } - return maskApiKey(compact); -} - -function formatCredentialSnippet(params: { - value: string | undefined; - ref: { source: string; id: string } | undefined; -}): string { - const value = typeof params.value === "string" ? params.value.trim() : ""; - if (value) { - return formatApiKeySnippet(value); - } - if (params.ref) { - return `ref(${params.ref.source}:${params.ref.id})`; - } - return "unknown"; -} - export function resolveModelAuthLabel(params: { provider?: string; cfg?: OpenClawConfig; @@ -69,13 +46,9 @@ export function resolveModelAuthLabel(params: { return `oauth${label ? ` (${label})` : ""}`; } if (profile.type === "token") { - return `token ${formatCredentialSnippet({ value: profile.token, ref: profile.tokenRef })}${ - label ? ` (${label})` : "" - }`; + return `token${label ? ` (${label})` : ""}`; } - return `api-key ${formatCredentialSnippet({ value: profile.key, ref: profile.keyRef })}${ - label ? ` (${label})` : "" - }`; + return `api-key${label ? ` (${label})` : ""}`; } const envKey = resolveEnvApiKey(providerKey); @@ -83,12 +56,12 @@ export function resolveModelAuthLabel(params: { if (envKey.source.includes("OAUTH_TOKEN")) { return `oauth (${envKey.source})`; } - return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`; + return `api-key (${envKey.source})`; } const customKey = getCustomProviderApiKey(params.cfg, providerKey); if (customKey) { - return `api-key ${formatApiKeySnippet(customKey)} (models.json)`; + return `api-key (models.json)`; } return "unknown"; diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 035866c20cd..9f183eeafe7 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -55,6 +55,7 @@ describe("registerPluginCommand", () => { { name: "demo_cmd", description: "Demo command", + acceptsArgs: false, }, ]); });