mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
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
This commit is contained in:
@@ -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<string, string> = {
|
||||
"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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Respon
|
||||
|
||||
if (config.verbose) {
|
||||
console.error(`> ${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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,13 @@ export async function ensureConfigDir(): Promise<void> {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export async function localSink(event: TrackingEvent): Promise<void> {
|
||||
// 文件还不存在,忽略
|
||||
}
|
||||
|
||||
appendFileSync(path, JSON.stringify(event) + "\n");
|
||||
appendFileSync(path, JSON.stringify(event) + "\n", { mode: 0o600 });
|
||||
} catch {
|
||||
// 埋点逻辑任何异常都不能影响 CLI 主流程
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user