Skip to content

Commit 3e4f1f0

Browse files
committed
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
1 parent 55f9fa2 commit 3e4f1f0

5 files changed

Lines changed: 24 additions & 4 deletions

File tree

packages/cli/src/commands/config/set.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
defineCommand,
33
detectOutputFormat,
4+
maskToken,
45
readConfigFile,
56
writeConfigFile,
67
BailianError,
@@ -28,6 +29,11 @@ const VALID_KEYS = [
2829
"workspace_id",
2930
];
3031

32+
// Keys whose values are secrets. Their stored value must never be echoed back in
33+
// cleartext (CI logs, pipes, shared terminals); show a masked form instead — the
34+
// same policy `config show` and `auth status` already follow.
35+
const SECRET_KEYS = new Set(["api_key", "access_token", "access_key_id", "access_key_secret"]);
36+
3137
// Allow hyphen-style keys (e.g. default-text-model → default_text_model)
3238
const KEY_ALIASES: Record<string, string> = {
3339
"base-url": "base_url",
@@ -120,7 +126,10 @@ export default defineCommand({
120126
await writeConfigFile(existing);
121127

122128
if (!config.quiet) {
123-
emitResult({ [resolvedKey]: existing[resolvedKey] }, format);
129+
const shown = SECRET_KEYS.has(resolvedKey)
130+
? maskToken(String(existing[resolvedKey]))
131+
: existing[resolvedKey];
132+
emitResult({ [resolvedKey]: shown }, format);
124133
}
125134
},
126135
});

packages/cli/src/commands/knowledge/retrieve.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
defineCommand,
33
signRequest,
44
detectOutputFormat,
5+
maskToken,
56
type Config,
67
type GlobalFlags,
78
type KnowledgeRetrieveRequest,
@@ -105,7 +106,7 @@ export default defineCommand({
105106

106107
if (config.verbose) {
107108
process.stderr.write(`> POST ${url}\n`);
108-
process.stderr.write(`> AK: ${accessKeyId.slice(0, 8)}...\n`);
109+
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
109110
}
110111

111112
const timeoutMs = config.timeout * 1000;

packages/core/src/client/http.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BailianError } from "../errors/base.ts";
44
import { ExitCode } from "../errors/codes.ts";
55
import { resolveCredential } from "../auth/resolver.ts";
66
import { mapApiError } from "../errors/api.ts";
7+
import { maskToken } from "../utils/token.ts";
78
import { SOURCE_CONFIG, trackingHeaders } from "./headers.ts";
89

910
export interface RequestOpts {
@@ -58,7 +59,7 @@ export async function request(config: Config, opts: RequestOpts): Promise<Respon
5859

5960
if (config.verbose) {
6061
console.error(`> ${opts.method ?? "GET"} ${opts.url}`);
61-
console.error(`> Auth: ${credential.token.slice(0, 8)}...`);
62+
console.error(`> Auth: ${maskToken(credential.token)}`);
6263
console.error(`> x-dashscope-source-config: ${SOURCE_CONFIG}`);
6364
}
6465
}

packages/core/src/config/paths.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,13 @@ export async function ensureConfigDir(): Promise<void> {
2020
const dir = getConfigDir();
2121
const fs = await import("fs/promises");
2222
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
23+
// `mkdir`'s `mode` only applies to directories it creates (and is masked by
24+
// umask). A config dir created by an older build or another tool may still be
25+
// world/group-readable while holding cleartext credentials, so tighten it
26+
// explicitly. Best-effort: never let a chmod failure break the command.
27+
try {
28+
await fs.chmod(dir, 0o700);
29+
} catch {
30+
/* best effort */
31+
}
2332
}

packages/core/src/telemetry/sink.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export async function localSink(event: TrackingEvent): Promise<void> {
9090
// 文件还不存在,忽略
9191
}
9292

93-
appendFileSync(path, JSON.stringify(event) + "\n");
93+
appendFileSync(path, JSON.stringify(event) + "\n", { mode: 0o600 });
9494
} catch {
9595
// 埋点逻辑任何异常都不能影响 CLI 主流程
9696
}

0 commit comments

Comments
 (0)