From 3e4f1f0ebf0d37347fe38bcdb21fb7a07c24cde0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:34:19 +0000 Subject: [PATCH] fix(security): stop leaking credentials and tighten on-disk permissions - config set: mask api_key/access_token/access_key_id/access_key_secret in the confirmation echo. It previously printed the stored secret verbatim to stdout (CI logs, pipes, screen shares), unlike `config show` / `auth status` which already maskToken(). - http / knowledge retrieve: use maskToken() in --verbose request logs instead of printing the first 8 chars of the bearer token / AccessKey id. - telemetry: write telemetry.jsonl with mode 0600 (was created world-readable by default), matching the other credential-area writers. - ensureConfigDir: chmod 0700 after mkdir, so a pre-existing ~/.bailian created by an older build/another tool (where mkdir's mode is ignored) holding cleartext credentials gets locked down too. Best-effort; never fatal. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1 --- packages/cli/src/commands/config/set.ts | 11 ++++++++++- packages/cli/src/commands/knowledge/retrieve.ts | 3 ++- packages/core/src/client/http.ts | 3 ++- packages/core/src/config/paths.ts | 9 +++++++++ packages/core/src/telemetry/sink.ts | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 004be0d..9a0288e 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -1,6 +1,7 @@ import { defineCommand, detectOutputFormat, + maskToken, readConfigFile, writeConfigFile, BailianError, @@ -28,6 +29,11 @@ const VALID_KEYS = [ "workspace_id", ]; +// Keys whose values are secrets. Their stored value must never be echoed back in +// cleartext (CI logs, pipes, shared terminals); show a masked form instead — the +// same policy `config show` and `auth status` already follow. +const SECRET_KEYS = new Set(["api_key", "access_token", "access_key_id", "access_key_secret"]); + // Allow hyphen-style keys (e.g. default-text-model → default_text_model) const KEY_ALIASES: Record = { "base-url": "base_url", @@ -120,7 +126,10 @@ export default defineCommand({ await writeConfigFile(existing); if (!config.quiet) { - emitResult({ [resolvedKey]: existing[resolvedKey] }, format); + const shown = SECRET_KEYS.has(resolvedKey) + ? maskToken(String(existing[resolvedKey])) + : existing[resolvedKey]; + emitResult({ [resolvedKey]: shown }, format); } }, }); diff --git a/packages/cli/src/commands/knowledge/retrieve.ts b/packages/cli/src/commands/knowledge/retrieve.ts index 349fb64..37dd073 100644 --- a/packages/cli/src/commands/knowledge/retrieve.ts +++ b/packages/cli/src/commands/knowledge/retrieve.ts @@ -2,6 +2,7 @@ import { defineCommand, signRequest, detectOutputFormat, + maskToken, type Config, type GlobalFlags, type KnowledgeRetrieveRequest, @@ -105,7 +106,7 @@ export default defineCommand({ if (config.verbose) { process.stderr.write(`> POST ${url}\n`); - process.stderr.write(`> AK: ${accessKeyId.slice(0, 8)}...\n`); + process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`); } const timeoutMs = config.timeout * 1000; diff --git a/packages/core/src/client/http.ts b/packages/core/src/client/http.ts index 1a30f30..22490b3 100644 --- a/packages/core/src/client/http.ts +++ b/packages/core/src/client/http.ts @@ -4,6 +4,7 @@ import { BailianError } from "../errors/base.ts"; import { ExitCode } from "../errors/codes.ts"; import { resolveCredential } from "../auth/resolver.ts"; import { mapApiError } from "../errors/api.ts"; +import { maskToken } from "../utils/token.ts"; import { SOURCE_CONFIG, trackingHeaders } from "./headers.ts"; export interface RequestOpts { @@ -58,7 +59,7 @@ export async function request(config: Config, opts: RequestOpts): Promise ${opts.method ?? "GET"} ${opts.url}`); - console.error(`> Auth: ${credential.token.slice(0, 8)}...`); + console.error(`> Auth: ${maskToken(credential.token)}`); console.error(`> x-dashscope-source-config: ${SOURCE_CONFIG}`); } } diff --git a/packages/core/src/config/paths.ts b/packages/core/src/config/paths.ts index 9e0de01..78e3f45 100644 --- a/packages/core/src/config/paths.ts +++ b/packages/core/src/config/paths.ts @@ -20,4 +20,13 @@ export async function ensureConfigDir(): Promise { const dir = getConfigDir(); const fs = await import("fs/promises"); await fs.mkdir(dir, { recursive: true, mode: 0o700 }); + // `mkdir`'s `mode` only applies to directories it creates (and is masked by + // umask). A config dir created by an older build or another tool may still be + // world/group-readable while holding cleartext credentials, so tighten it + // explicitly. Best-effort: never let a chmod failure break the command. + try { + await fs.chmod(dir, 0o700); + } catch { + /* best effort */ + } } diff --git a/packages/core/src/telemetry/sink.ts b/packages/core/src/telemetry/sink.ts index d7ecc16..7ecf529 100644 --- a/packages/core/src/telemetry/sink.ts +++ b/packages/core/src/telemetry/sink.ts @@ -90,7 +90,7 @@ export async function localSink(event: TrackingEvent): Promise { // 文件还不存在,忽略 } - appendFileSync(path, JSON.stringify(event) + "\n"); + appendFileSync(path, JSON.stringify(event) + "\n", { mode: 0o600 }); } catch { // 埋点逻辑任何异常都不能影响 CLI 主流程 }