{"repo_url":"https://github.com/farion1231/cc-switch","overall_status":"completed","scanned_at":"2026-10-14T23:02:53.521Z","scan_summary":{"total_components":1,"worst_case_severity":"HIGH","repo_trust_score":{"score":69,"tier":"medium","capability_band":"extreme","score_confidence":"low","score_interval":{"low":49,"high":89},"factors":["ATR-C0004 MEDIUM","ATR-C0007 HIGH","ATR-C0012 MEDIUM"]}},"components":[{"artifact_version":"2.0","component":{"component_type":"mcp-server","component_name":"mcp-server","component_path":".","metadata":{"transports":["stdio"]}},"scan_provenance":{"scanned_at":"2026-10-14T23:02:53.521Z","resolved_ref":"51ac86f1c59abfefabadebd05ec5a5415b8ad72c","resolved_ver":null},"repo_context":{"repo_url":"https://github.com/farion1231/cc-switch","languages":["Rust","TypeScript"],"transports_detected":[],"deployment_artifacts":[],"provenance":{"publisher":"farion1231","publisher_verified":false,"signed_build":false,"dependency_cve_count":31,"maintenance_active":true,"declared_policy":false}},"component_findings":[{"rule_id":"ATR-M0001","rule_version":"1.2.0","rule_name":"Tool Definition Integrity & Versioning (ETDI-style)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"This rule's threat model (unsigned/hot-reloadable MCP tool manifests consumed by an LLM agent) does not apply: cc-switch exposes no MCP tool manifests to any external MCP client/agent at all. Its Tauri IPC 'commands' are statically compiled into the desktop binary and invoked only by its own bundled React frontend running in the same process/webview -- there is no MCP tools/list exchange, no runtime tool-definition hot-reload, and no LLM agent making autonomous tool-selection decisions based on tool descriptions. To the extent an analogue applies, the binary itself is distributed via a code-signed updater (minisign pubkey embedded in tauri.conf.json, updates fetched only from the declared HTTPS endpoints and verified against that pubkey before install), matching the false_positive_guard for 'offline, immutable tools embedded in binary with code-signed releases.'","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents rug-pull tool poisoning.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"src-tauri/tauri.conf.json","start_line":63,"end_line":68,"snippet":"\"updater\": {\n  \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM4MDI4QzlBNTczOTI4RTMKUldUaktEbFhtb3dDeUM5US9kT0FmdGR5Ti9vQzcwa2dTMlpibDVDUmQ2M0VGTzVOWnd0SGpFVlEK\",\n  \"endpoints\": [\n    \"https://dl.ccswitch.io/latest.json\",\n    \"https://github.com/farion1231/cc-switch/releases/latest/download/latest.json\"\n  ]\n}"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0002","rule_version":"1.1.0","rule_name":"Unrestricted Tool Permissions (Overprivileged Tools)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"The rule targets MCP tools callable by an autonomous LLM agent with scope broader than its declared purpose. cc-switch's IPC commands (e.g. run_tool_lifecycle_action, which can execute curl|bash-style installers) are invoked only by explicit user clicks in cc-switch's own GUI -- there is no LLM/agent deciding to invoke them based on a tool description, so the 'overprivileged tool selected by a reasoning model' threat model does not apply. Within its own (non-MCP) threat model the command surface is reasonably scoped: the tool/action identifiers are validated against a fixed VALID_TOOLS allowlist (claude/codex/gemini/grok/opencode/openclaw/hermes/pi/mcode) before any shell command is constructed, rather than accepting arbitrary caller-supplied commands.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Limits blast radius and privilege escalation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/commands/misc.rs","start_line":138,"end_line":140,"snippet":"const VALID_TOOLS: [&str; 9] = [\n    \"claude\", \"codex\", \"gemini\", \"grok\", \"opencode\", \"openclaw\", \"hermes\", \"pi\", \"mcode\",\n];","vulnerability":{}},{"path":"src-tauri/src/commands/misc.rs","start_line":283,"end_line":285,"snippet":"    if !VALID_TOOLS.contains(&tool.as_str()) {\n        return Err(format!(\"Unsupported tool action target: {tool}\"));\n    }","vulnerability":{}}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0003","rule_version":"1.3.1","rule_name":"CORS / Origin / Host Validation (DNS Rebinding Defense)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"cc-switch does not implement an MCP server with a remote transport (SSE/streamable_http). It is a local configuration manager; its only HTTP listener is a local reverse-proxy (axum) bound by default to 127.0.0.1, used solely to translate between LLM provider API wire formats for request routing/failover -- it carries no MCP tools/list or tools/call semantics and is not an MCP transport endpoint. Rule applicability requires_remote_transport=true; this component's sole externally-reachable surface is this unrelated local proxy, not an MCP remote transport.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents DNS rebinding/browser hijack of local or remote MCP.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/types.rs","start_line":42,"end_line":52,"snippet":"impl Default for ProxyConfig {\n    fn default() -> Self {\n        Self {\n            listen_address: \"127.0.0.1\".to_string(),\n            listen_port: 15721, // 使用较少占用的高位端口\n            max_retries: 3,\n            request_timeout: 600,\n            enable_logging: true,\n..."}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0004","rule_version":"1.2.0","rule_name":"Binding to 0.0.0.0 Without Auth/TLS","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No MCP server transport exists in this codebase to bind to 0.0.0.0. The only HTTP listener is the local LLM-provider reverse proxy, which defaults to 127.0.0.1 (ProxyConfig::default, src-tauri/src/proxy/types.rs:45) and is stored per-app in a local SQLite DB (src-tauri/src/database/dao/proxy.rs), not exposed as an MCP endpoint. Rule requires_remote_transport=true, which this MCP-adjacent configuration-manager component does not have.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"LAN/Internet exposure enables remote exploitation.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/types.rs","start_line":42,"end_line":52,"snippet":"impl Default for ProxyConfig {\n    fn default() -> Self {\n        Self {\n            listen_address: \"127.0.0.1\".to_string(),\n            listen_port: 15721,\n..."}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0005","rule_version":"1.4.0","rule_name":"OAuth 2.1 + PKCE Correctness (MCP Authorization)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"This component is not a remote MCP server; it has no MCP authorization surface subject to the MCP Authorization spec. OAuth flows found in the codebase (codex_oauth.rs, xai_oauth.rs) are for authenticating the DESKTOP APP as a client of upstream vendor APIs (ChatGPT/xAI subscription quota), not an MCP server authorizing incoming MCP client connections. requires_remote_transport=true is not met.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Misconfig enables token theft and confused-deputy attacks.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/commands/codex_oauth.rs","start_line":1,"end_line":20,"snippet":"//! Codex OAuth Tauri Commands\n//!\n//! 提供 OpenAI ChatGPT Plus/Pro OAuth 认证相关的 Tauri 命令。\n//! ... Codex OAuth 专属的订阅额度和模型列表查询命令。\npub struct CodexOAuthState(pub Arc<CodexOAuthManager>);"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0006","rule_version":"1.2.0","rule_name":"Tool Name Collision / Spoofing","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"cc-switch registers no tools into any MCP tool namespace visible to an LLM client, so there is no possibility of colliding with or spoofing another MCP server's tool name in a shared agent context. Its IPC command names (open_external, upsert_mcp_server, etc.) are internal Rust function identifiers invoked only via Tauri's own generate_handler! dispatch from its bundled frontend, never exposed to any multi-server MCP aggregation context where name collision could occur.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents malicious tool shadowing.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/lib.rs","start_line":1429,"end_line":1430,"snippet":"        .invoke_handler(tauri::generate_handler!["}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0007","rule_version":"1.2.0","rule_name":"Prompt/Metadata Injection in Tool Descriptions","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"No MCP tool descriptions are produced by this component for consumption by an LLM's context window -- there is no tools/list response, docstring-to-model pipeline, or any other mechanism by which this repo's internal Rust doc-comments / IPC command names reach a model's prompt context. The rule's threat model (hidden directives/ANSI escapes/RLO in a tool description manipulating an LLM) therefore cannot manifest here.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents covert model manipulation.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/commands/mcp.rs","start_line":14,"end_line":18,"snippet":"/// 获取 Claude MCP 状态\n#[tauri::command]\npub async fn get_claude_mcp_status() -> Result<claude_mcp::McpStatus, String> {\n    claude_mcp::get_mcp_status().map_err(|e| e.to_string())\n}"}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0008","rule_version":"1.2.0","rule_name":"CORS Misuse: Wildcard with Credentials","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No MCP remote transport exists; grep across src-tauri/src found no CorsLayer/tower_http::cors usage at all. The local proxy router (src-tauri/src/proxy/server.rs) sets up axum routes for LLM API translation without any CORS layer because it is intended for same-origin CLI clients hitting localhost, not a browser-exposed MCP endpoint. requires_remote_transport=true not met for this component.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents credentialed cross-origin abuse.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/server.rs","start_line":300,"end_line":310,"snippet":"let app = Router::new()\n    .route(\"/health\", get(handlers::health_check))\n    .route(\"/status\", get(handlers::get_status))\n    .route(\"/v1/messages\", post(handlers::handle_messages))\n    .route(\"/claude/v1/messages\", post(handlers::handle_messages))\n..."}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0009","rule_version":"1.3.0","rule_name":"Transport Security: TLS Everywhere (Remote)","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"No remote MCP transport is present. The local reverse-proxy listens on 127.0.0.1 and is consumed only by locally-running CLI tools on the same machine, not remote MCP clients; TLS termination is irrelevant to loopback-only local IPC-equivalent traffic. requires_remote_transport=true not met.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"MitM can steal tokens and tamper responses.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/types.rs","start_line":42,"end_line":50,"snippet":"impl Default for ProxyConfig {\n    fn default() -> Self {\n        Self {\n            listen_address: \"127.0.0.1\".to_string(),\n            listen_port: 15721,\n..."}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0010","rule_version":"1.4.0","rule_name":"No Authentication or Authorization Checks (General)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"No remote-facing MCP server surface exists to require authn/authz. The app's Tauri IPC commands are invoked only by its own bundled frontend inside the same desktop-app process boundary (standard Tauri webview<->Rust IPC, gated by the OS process and Tauri's capability/permission system, src-tauri/capabilities/default.json), not reachable by external network MCP clients. The local HTTP proxy (127.0.0.1) is not an MCP endpoint and is used by locally co-located CLI processes the user explicitly configured. requires_remote_transport=true is not met for this component.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents unauthorized tool use and data access.","context_note":null,"within_declared_contract":null,"transport_gated":true,"mitigation_present":null,"evidence":[{"path":"src-tauri/capabilities/default.json","start_line":1,"end_line":20,"snippet":"{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"default\",\n  \"description\": \"enables the default permissions\",\n  \"windows\": [\"main\"],\n  \"permissions\": [\n    \"core:default\",\n    \"opener:default\",\n    \"updater:default\",\n    \"log:default\",\n..."}],"affected_tools":null,"component_name":"mcp-server"},{"rule_id":"ATR-M0011","rule_version":"1.2.0","rule_name":"OAuth Proxy Per-Client Consent (Confused Deputy Defense)","finding":"NOT_APPLICABLE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"cc-switch is not an OAuth proxy fronting an upstream IdP on behalf of dynamically-registered MCP clients. Its OAuth code (codex_oauth.rs, xai_oauth.rs) authenticates the single local desktop app instance as a direct client of vendor APIs (ChatGPT, xAI) for the user's own account -- there is no MCP client registration endpoint, no bridge-code minting for third parties, and no shared client_id serving multiple distinct MCP clients. Single-tenant deployment with no registration path (false_positive_guard: Q2=no) applies.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/commands/codex_oauth.rs","start_line":16,"end_line":20,"snippet":"pub struct CodexOAuthState(pub Arc<CodexOAuthManager>);\n\n/// 查询 Codex OAuth (ChatGPT Plus/Pro) 订阅额度"}],"affected_tools":null,"component_name":"mcp-server"}],"repo_findings_inherited":[{"rule_id":"ATR-C0001","rule_version":"1.2.0","rule_name":"Unrestricted Network Fetch / SSRF","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"The one clearly attacker-influenced outbound-fetch surface (deep-link iconUrl, fully attacker-controlled via a ccswitch:// URL) is hardened against SSRF: https-only, no credentials/non-default port, DNS resolution restricted to public (non-RFC1918/loopback/link-local/CGNAT/multicast) addresses with address pinning across redirects, max 3 redirects each re-validated, 5s timeout, 256KB cap, no proxy, no cookies/referrer. Other outbound calls (model_fetch.rs, proxy/forwarder.rs) target user-configured provider base URLs that the local user explicitly typed into their own provider profile (an admin/owner input, not a remote attacker's), consistent with the app's threat model as a local desktop tool.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Enables internal pivoting and data exfiltration.","context_note":null,"within_declared_contract":true,"transport_gated":true,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/deeplink/icon_fetch.rs","start_line":59,"end_line":143,"snippet":"/// 链接里的 `iconUrl` 形状是否可用：https、域名主机、默认端口、没有账号密码。\n/// 解析阶段用它过滤，下载时每一跳重定向也再过一遍。\npub(crate) fn parse_icon_url(raw: &str) -> Option<Url> {\n    let raw = raw.trim();\n    ...\n        return None;\n    }\n    let url = Url::parse(raw).ok()?;\n    !(a == 0 || a == 10 || a == 127 || (a == 100 && (64..128).contains(&b)) ...)\n        || !url.username().is_empty()"},{"path":"src-tauri/src/deeplink/icon_fetch.rs","start_line":302,"end_line":389,"snippet":"/// 解析域名，要求每个地址都是公网地址。有一个不是就整体拒绝，不挑着用。\n    let addrs: Vec<SocketAddr> = tokio::net::lookup_host((domain.as_str(), 443)).await...;\n    if let Some(addr) = addrs.iter().find(|addr| !is_public_ip(addr.ip())) { return Err(...); }\n    ...\n    };\n    let domain = domain.to_string();\n    ... .resolve_to_addrs(&domain, &addrs) ... .redirect(reqwest::redirect::Policy::none()) ..."}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0002","rule_version":"1.3.2","rule_name":"Static Secrets","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"gitleaks (20 findings) and semgrep's google-oauth-token rule (21 findings) flagged only fixture/test values: unit-test constants in auth.rs/codex.rs/gemini.rs (e.g. 'ya29.access-token-12345', 'sk-test-key-12345678') used to test masking/parsing logic, and the deeplink.html generator which is a client-side form template whose placeholder example values (e.g. 'AIzaSy-test-key123') are for documentation/demo purposes in a static HTML generator page, not runtime secrets. No live API keys, private keys, or credentials were found committed to the repo.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Plaintext secrets are easily exfiltrated.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/proxy/providers/auth.rs","start_line":189,"end_line":200,"snippet":"#[test]\n    fn test_auth_info_with_access_token() {\n        let auth = AuthInfo::with_access_token(\n            \"refresh-token\".to_string(),\n            \"ya29.access-token-12345\".to_string(),\n        );\n        assert_eq!(auth.api_key, \"refresh-token\");\n        assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth);"},{"path":"deplink.html","start_line":885,"end_line":896,"snippet":"GEMINI_API_KEY\": \"[REDACTED]\"  <!-- example payload shown in a static HTML usage-script generator, not a real credential -->"},{"path":"deplink.html","start_line":889,"end_line":889,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":894,"end_line":894,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":895,"end_line":895,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":448,"end_line":448,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":489,"end_line":489,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":433,"end_line":433,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":439,"end_line":439,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":475,"end_line":475,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":481,"end_line":481,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":512,"end_line":512,"snippet":null,"vulnerability":{}},{"path":"deplink.html","start_line":516,"end_line":516,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1569,"end_line":1569,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1574,"end_line":1574,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1602,"end_line":1602,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1607,"end_line":1607,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1907,"end_line":1907,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1927,"end_line":1927,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/gemini.rs","start_line":299,"end_line":299,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/providers/gemini.rs","start_line":304,"end_line":304,"snippet":null,"vulnerability":{}},{"path":"src-tauri/src/proxy/server.rs","start_line":503,"end_line":503,"snippet":null,"vulnerability":{}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0003","rule_version":"1.3.0","rule_name":"Runtime Command Injection via Process Execution","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"Extensively reviewed every std::process::Command call site in src-tauri/src (terminal launch, tool install/update lifecycle, WSL execution, mcode/codex installers). The two highest-risk candidates are hardened: (1) run_tool_lifecycle_silently/build_tool_lifecycle_command executes installer commands, but `tool`/`action` are validated against a fixed VALID_TOOLS allowlist (normalize_requested_tools) before any command string is built, and install/update command strings are built entirely from hardcoded consts (CLAUDE_INSTALL_UNIX, etc.) -- no caller-supplied string is spliced into the shell command. (2) WSL distro/shell/shell-flag inputs (which DO come from frontend WslShellPreferenceInput) are validated by is_valid_wsl_distro_name/is_valid_shell/is_valid_shell_flag allowlists before being placed into a `wsl.exe -d <distro> -- <shell> <flag> <command>` argv (not a shell string), rejecting anything with shell metacharacters. The one unescaped-shell-interpolation path found (`launch_custom`/'{command}' template substitution in terminal/mod.rs) is explicitly documented as UNREACHABLE dead code (no UI wires a 'custom' terminal target or customConfig), confirmed by grep showing launch_session_terminal never passes 'custom'/'Some(custom_config)' from the frontend.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/commands/misc.rs","start_line":138,"end_line":140,"snippet":"const VALID_TOOLS: [&str; 9] = [\n    \"claude\", \"codex\", \"gemini\", \"grok\", \"opencode\", \"openclaw\", \"hermes\", \"pi\", \"mcode\",\n];","vulnerability":{}},{"path":"src-tauri/src/commands/misc.rs","start_line":503,"end_line":510,"snippet":"fn normalize_requested_tools(tools: &[String]) -> Vec<&'static str> {\n    let set: std::collections::HashSet<&str> = tools.iter().map(|s| s.as_str()).collect();\n    VALID_TOOLS\n        .iter()\n        .copied()\n        .filter(|tool| set.contains(tool))\n        .collect()\n}","vulnerability":{}},{"path":"src-tauri/src/commands/misc.rs","start_line":1039,"end_line":1063,"snippet":"fn build_wsl_tool_action_line(distro: &str, command: &str, force_shell: Option<&str>, force_shell_flag: Option<&str>) -> Result<String, String> {\n    if !is_valid_wsl_distro_name(distro) { return Err(...); }\n    let shell = force_shell.map(...).unwrap_or(\"sh\");\n    if !is_valid_shell(shell) { return Err(...); }\n    ...\n    Ok(format!(\"wsl.exe -d {distro} -- {shell} {flag} {}\", windows_cmd_double_quote_arg(command)))\n}","vulnerability":{}},{"path":"src-tauri/src/session_manager/terminal/mod.rs","start_line":398,"end_line":406,"snippet":"// 目前本分支无 UI 入口(终端选项列表没有 `custom`，前端也从不传\n// `customConfig`)，所以不可达。**接线前必须换掉这个方案**"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0004","rule_version":"1.2.0","rule_name":"DoS Protections: Timeouts, Concurrency, Streaming Limits","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"medium","rationale":"The proxy server binds the configured listen_address verbatim, so the listener is not restricted to loopback by construction. Its accept loop spawns an independent Tokio task for every accepted connection with no semaphore, connection cap, or rate limiter. Each request body is capped at 200MB, but that cap bounds only a single request's size, not the number of concurrent connections or tasks; when the listener is reachable beyond the local host, many simultaneous connections can accumulate unbounded tasks and memory without ever hitting the per-request cap.","vulnerability_details":null,"fix_guidance":"Add a concurrency guard around the proxy's accept loop (e.g., a semaphore or bounded connection pool) that limits the number of simultaneously in-flight connections/tasks, and consider additional rate limiting when listen_address is configured to a non-loopback value.","why_this_matters":"Resource exhaustion can crash or stall MCP.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0004 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/server.rs","start_line":104,"end_line":120,"snippet":"        if self.shutdown_tx.read().await.is_some() {\n            return Err(ProxyError::AlreadyRunning);\n        }\n\n        let addr: SocketAddr =\n            format!(\"{}:{}\", self.config.listen_address, self.config.listen_port)\n                .parse()\n                .map_err(|e| ProxyError::BindFailed(format!(\"无效的地址: {e}\")))?;\n\n        // 创建关闭通道\n        let (shutdown_tx, shutdown_rx) = oneshot::channel();\n\n        // 构建路由\n        let app = self.build_router();\n\n        // 绑定监听器\n        let listener = tokio::net::TcpListener::bind(&addr)"},{"path":"src-tauri/src/proxy/server.rs","start_line":151,"end_line":170,"snippet":"            loop {\n                tokio::select! {\n                    result = listener.accept() => {\n                        let (stream, _remote_addr) = match result {\n                            Ok(v) => v,\n                            Err(e) => {\n                                log::error!(\"[{SRV}] accept 失败: {e}\", SRV = log_srv::ACCEPT_ERR);\n                                tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n                                continue;\n                            }\n                        };\n\n                        let app = app.clone();\n                        tokio::spawn(async move {\n                            // Peek raw TCP bytes to capture original header casing\n                            // before hyper parses (and lowercases) the header names.\n                            let original_cases = {\n                                let mut peek_buf = vec![0u8; 8192];\n                                match stream.peek(&mut peek_buf).await {\n                                    Ok(n) => {"},{"path":"src-tauri/src/proxy/server.rs","start_line":419,"end_line":425,"snippet":"            .route(\"/gemini/v1beta/*path\", any(handlers::handle_gemini))\n            // Gemini 的 GA 版本也叫 /v1，给原 SDK 留一条出口\n            .route(\"/gemini/v1/*path\", any(handlers::handle_gemini))\n            // 提高默认请求体大小限制（避免 413 Payload Too Large）\n            .layer(DefaultBodyLimit::max(200 * 1024 * 1024))\n            .with_state(self.state.clone())\n    }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0005","rule_version":"1.2.0","rule_name":"Logging Hygiene (No Secrets/PII/Tokens) & Audit Trail","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The codebase shows deliberate log-redaction engineering: icon_fetch.rs strips full URLs (which may carry CDN signature tokens) from reqwest errors before logging via describe_request_error()/url_for_log(), with an explicit unit test asserting a secret query-param token does not leak into the logged string. This reflects a project-wide pattern of treating request errors as a logging-injection surface and redacting it.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents sensitive data leakage; enables forensics.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/deeplink/icon_fetch.rs","start_line":288,"end_line":300,"snippet":"/// reqwest 的错误文本自带完整 URL（含查询参数，可能是 CDN 签名或令牌），而重定向之后的\n/// 地址也没经过 `url_for_log`。去掉 URL，只留错误类型和底层原因写进日志。\nfn describe_request_error(error: reqwest::Error) -> String {\n    let error = error.without_url();\n    let mut text = error.to_string();"},{"path":"src-tauri/src/deeplink/icon_fetch.rs","start_line":593,"end_line":609,"snippet":"async fn request_errors_leave_the_url_out_of_the_log() {\n        ... .get(\"http://127.0.0.1:9/logo.png?token=SECRET_TOKEN\") ...\n        assert!(error.to_string().contains(\"SECRET_TOKEN\"));\n        let described = describe_request_error(error);\n        assert!(!described.contains(\"SECRET_TOKEN\"), \"{described}\");"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0006","rule_version":"1.2.0","rule_name":"Container/Runtime Hardening","finding":"NOT_APPLICABLE","severity":"MEDIUM","realized_severity":null,"confidence":"high","rationale":"This is a Tauri desktop application (macOS/Windows/Linux native installer: .dmg/.msi/.deb/flatpak) with no Dockerfile, docker-compose, Kubernetes manifests, or other container/IaC artifacts found in the repository. Trivy config scan (trivy-config.json) returned zero findings, confirming no IaC surface exists. The rule's container-hardening threat model (securityContext, capabilities, mounts, exposed ports in container orchestration) does not apply to a locally-installed desktop binary.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Reduces blast radius of exploits.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0007","rule_version":"1.2.0","rule_name":"Dependency Risk & Supply Chain","finding":"VULNERABLE","severity":"HIGH","realized_severity":"HIGH","confidence":"high","rationale":"Trivy fs scan of committed lockfiles at HEAD found known HIGH-severity CVEs in dependencies that are actually linked into the shipped binary. `reqwest = { version = \"0.12\", features = [\"rustls-tls\", \"json\", \"stream\", \"socks\"] }` in src-tauri/Cargo.toml does not set default-features=false, so reqwest's default `native-tls` feature (pulling openssl/openssl-sys) and `http3`-adjacent quinn stack are compiled in alongside rustls, and aws-lc-rs (depending on aws-lc-sys) is pulled transitively by the rustls/ring crypto stack -- all appear as real resolved packages in Cargo.lock, not optional/dev-only entries. No override, patch, or vendoring addresses the flagged versions (aws-lc-sys 0.36.0, openssl 0.10.75, quinn-proto 0.11.14, smol-toml in pnpm-lock.yaml), so the vulnerable code ships in the application's TLS/crypto/transport path used for every outbound HTTPS request (deep-link icon fetch, provider model-list fetch, proxy forwarding) and the npm toolchain's TOML parsing dependency.","vulnerability_details":null,"fix_guidance":"Set `reqwest = { version = \"0.12\", default-features = false, features = [\"rustls-tls\", \"json\", \"stream\", \"socks\"] }` to drop the native-tls/openssl and quinn/http3 transitive pulls entirely if unused, or run `cargo update -p aws-lc-sys -p openssl -p quinn-proto` to patched versions; run `pnpm update smol-toml` similarly. Re-run trivy fs to confirm CVEs are cleared.","why_this_matters":"Prevents typosquat and malicious update compromise.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0007 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals HIGH.","within_declared_contract":false,"transport_gated":false,"mitigation_present":false,"evidence":[{"path":"src-tauri/Cargo.lock","start_line":346,"end_line":348,"snippet":"[[package]]\nname = \"aws-lc-sys\"\nversion = \"0.36.0\""},{"path":"src-tauri/Cargo.toml","start_line":42,"end_line":42,"snippet":"reqwest = { version = \"0.12\", features = [\"rustls-tls\", \"json\", \"stream\", \"socks\"] }"},{"path":"src-tauri/Cargo.lock","start_line":4422,"end_line":4449,"snippet":"name = \"reqwest\"\n...\ndependencies = [\n ...\n \"hyper-rustls\",\n \"hyper-tls\",\n ...\n \"native-tls\",\n ...\n \"quinn\",\n \"rustls\",\n ...\n]"},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"CVE-2026-85730","pkg_name":"smol-toml","severity":"HIGH","installed_version":"1.4.2","fixed_version":"1.7.1"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-r4xh-jqrq-34v2","pkg_name":"smol-toml","severity":"MEDIUM","installed_version":"1.4.2","fixed_version":"1.9.0"}},{"path":"pnpm-lock.yaml","start_line":0,"end_line":0,"snippet":null,"vulnerability":{"vulnerability_id":"GHSA-v3rj-xjv7-4jmq","pkg_name":"smol-toml","severity":"MEDIUM","installed_version":"1.4.2","fixed_version":"1.6.1"}}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0008","rule_version":"1.2.0","rule_name":"Missing or Inadequate Input Validation","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Reviewed the main attacker-reachable input boundaries: deep-link (ccswitch://) import parsing validates resource type, required fields, URL scheme (utils::validate_url restricts to http/https), and Base64 decoding with explicit error handling (deeplink/utils.rs, deeplink/provider.rs); workspace file commands use fixed filename allowlists (ALLOWED_FILES) and regex-validated daily-memory filenames; skill install paths sanitize path components rejecting '.', '..', and absolute/prefixed segments (sanitize_skill_source_path, sanitize_install_name). Tauri commands take typed/serde-deserialized structs which reject malformed JSON shapes at the IPC boundary. No evidence of raw/untyped user input reaching sensitive sinks without a validation step.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Frontline control to prevent multiple classes of vulns.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/deeplink/utils.rs","start_line":10,"end_line":22,"snippet":"pub fn validate_url(url_str: &str, field_name: &str) -> Result<(), AppError> {\n    let url = Url::parse(url_str)...;\n        .map_err(|e| AppError::InvalidInput(format!(\"Invalid URL for '{field_name}': {e}\")))?;\n    if scheme != \"http\" && scheme != \"https\" { return Err(...); }\n    let scheme = url.scheme();\n    if scheme != \"http\" && scheme != \"https\" {"},{"path":"src-tauri/src/commands/workspace.rs","start_line":9,"end_line":28,"snippet":"const ALLOWED_FILES: &[&str] = &[\"AGENTS.md\", \"SOUL.md\", ...];\nfn validate_filename(filename: &str) -> Result<(), String> {\n    if !ALLOWED_FILES.contains(&filename) { return Err(...); }\n    Ok(())\n}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0009","rule_version":"1.2.0","rule_name":"Insecure Deserialization","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"All deserialization observed uses serde_json (and serde-derived structs) for JSON parsing of deep-link payloads, provider/MCP configs, and proxy request/response bodies. Rust's serde_json is a memory-safe, schema-driven deserializer with no reflective/gadget-chain execution capability (unlike Python pickle, Java ObjectInputStream, or PHP unserialize), so even fully attacker-controlled JSON cannot achieve code execution via deserialization alone. No use of unsafe deserializers (bincode from untrusted sources, serde_yaml of untrusted remote content, eval-based parsing) was found reachable from attacker-controlled input.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Can lead to arbitrary code execution.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/deeplink/mcp.rs","start_line":73,"end_line":76,"snippet":"// Parse JSON\n    let config_json: Value = serde_json::from_str(&config_str)\n        .map_err(|e| AppError::InvalidInput(format!(\"Invalid JSON in MCP config: {e}\")))?;"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0010","rule_version":"1.2.0","rule_name":"Lack of Output Encoding/Sanitization","finding":"SECURE","severity":"MEDIUM","realized_severity":null,"confidence":"medium","rationale":"The single dangerouslySetInnerHTML sink in the React frontend (ProviderIcon.tsx) renders SVG strings sourced exclusively from the app's own bundled icon registry (@/icons/extracted via getIcon()/hasIcon()), gated so that custom-icon and URL-based icons are routed to an <img src=...> element instead (which does not execute inline content) rather than dangerouslySetInnerHTML. No user-controlled or remote string is ever passed to dangerouslySetInnerHTML. React's default JSX text rendering elsewhere auto-escapes output.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Prevents UI-level injection and operator compromise.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src/components/ProviderIcon.tsx","start_line":32,"end_line":37,"snippet":"const iconSvg = useMemo(() => {\n    if (icon && !isCustomIcon(icon) && !isUrlIcon(icon) && hasIcon(icon)) {\n      return getIcon(icon);\n    }\n    return \"\";\n  }, [icon]);"},{"path":"src/components/ProviderIcon.tsx","start_line":80,"end_line":109,"snippet":"  if (iconSvg) {\n    return (\n      <span ... dangerouslySetInnerHTML={{ __html: iconSvg }} />\n        className={cn(\n          \"inline-flex items-center justify-center flex-shrink-0\",\n          className,\n        )}\n      <img src={iconUrl} onError={...} alt={name} ... />\n        style={{ ...sizeStyle, color: effectiveColor }}\n        dangerouslySetInnerHTML={{ __html: iconSvg }}"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0011","rule_version":"1.4.0","rule_name":"Path Traversal & Symlink Bypass (Filesystem Tools)","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"high","rationale":"Enumerated the caller-supplied-path entry points: (1) skill install source paths -- sanitize_skill_source_path rejects '.', '..', absolute/root/prefix components, normalizing to only Normal path segments; (2) skill install names -- sanitize_install_name additionally rejects '/' and '\\\\' literal characters (to prevent Windows-vs-Unix separator semantic mismatches) and leading-dot names; (3) workspace file commands -- fixed ALLOWED_FILES allowlist / regex-anchored daily-memory filename pattern, no path join from raw caller input; (4) zip/archive extraction (skill repo download and local-zip import) -- uses `enclosed_name()` as a first gate AND an explicit second check rejecting any Component::ParentDir in the post-root-stripped relative path (the code comments show the developer specifically reasoned through and closed the 'repo-main/../evil' bypass that enclosed_name() alone would miss), plus symlink-target self-containment verification before resolving symlink entries. Each entry point is covered by either an allowlist or a structural traversal/symlink gate; the implementation explicitly documents and defends against partial-mitigation bypass patterns.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Traversal exposes arbitrary host files; can lead to code execution.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/services/skill.rs","start_line":3151,"end_line":3182,"snippet":"fn sanitize_skill_source_path(raw: &str) -> Option<PathBuf> {\n    ...\n    for component in Path::new(trimmed).components() {\n        match component {\n            Component::Normal(name) => { ... }\n            Component::CurDir | Component::ParentDir | Component::RootDir | Component::Prefix(_) => { return None; }\n        }\n    }"},{"path":"src-tauri/src/services/skill.rs","start_line":3772,"end_line":3789,"snippet":"// GitHub 归档统一带一层 `<repo>-<branch>/` 根目录，需剚掉后再落盘。\n let Ok(relative_path) = safe_path.strip_prefix(&root_name) else { continue; };\n // 第二道：enclosed_name() 的保证是相对归档根的，且它不规范化路径\n if relative_path.components().any(|c| matches!(c, Component::ParentDir)) {\n     log::warn!(\"跳过越界的压缩包条目: {}\", file.name());\n     continue;\n }"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0012","rule_version":"1.2.0","rule_name":"Denial of Wallet / Cost Controls","finding":"VULNERABLE","severity":"MEDIUM","realized_severity":"MEDIUM","confidence":"low","rationale":"The local HTTP proxy binds the configured listen address and exposes POST routes such as /v1/messages without any caller authentication. The JSON body handler does not enforce a Content-Type check, so a simple cross-origin POST request containing JSON text can reach it. Accepted requests are forwarded through forward_with_retry to the configured upstream provider using the stored provider credentials, so any caller able to reach the listener can trigger billed upstream requests on the user's behalf. The 200MB body size cap and per-request timeout bound the size and duration of a single request, but they do not implement caller authentication, a request-rate limit, or a spend budget, so they do not prevent unauthenticated cost-generating traffic when the proxy is reachable beyond strictly local use.","vulnerability_details":null,"fix_guidance":"Require caller authentication (e.g., a shared local token verified on every proxy request) before forwarding to the upstream provider, enforce a strict Content-Type check on incoming bodies to reject simple cross-origin requests, and add a request-rate or spend-budget limiter to the proxy routes.","why_this_matters":"Prevents cost-based disruption.","context_note":"[capability_by_design|relief:none|ineligible:within_declared_contract] within_declared_contract is ineligible for relief on ATR-C0012 because the rule declares that signal orthogonal to its risk (discount_ineligible_signals); the reported behaviour is within the tool's declared capability, and that does not reduce this finding's severity. No context relief applies; realized severity equals MEDIUM.","within_declared_contract":null,"transport_gated":null,"mitigation_present":null,"evidence":[{"path":"src-tauri/src/proxy/server.rs","start_line":104,"end_line":123,"snippet":"        if self.shutdown_tx.read().await.is_some() {\n            return Err(ProxyError::AlreadyRunning);\n        }\n\n        let addr: SocketAddr =\n            format!(\"{}:{}\", self.config.listen_address, self.config.listen_port)\n                .parse()\n                .map_err(|e| ProxyError::BindFailed(format!(\"无效的地址: {e}\")))?;\n\n        // 创建关闭通道\n        let (shutdown_tx, shutdown_rx) = oneshot::channel();\n\n        // 构建路由\n        let app = self.build_router();\n\n        // 绑定监听器\n        let listener = tokio::net::TcpListener::bind(&addr)\n            .await\n            .map_err(|e| ProxyError::BindFailed(e.to_string()))?;\n        let local_addr = listener"},{"path":"src-tauri/src/proxy/server.rs","start_line":298,"end_line":307,"snippet":"\n    fn build_router(&self) -> Router {\n        Router::new()\n            // 健康检查\n            .route(\"/health\", get(handlers::health_check))\n            .route(\"/status\", get(handlers::get_status))\n            // Claude API (支持带前缀和不带前缀两种格式)\n            .route(\"/v1/messages\", post(handlers::handle_messages))\n            .route(\"/claude/v1/messages\", post(handlers::handle_messages))\n            // Claude Desktop 3P 本地 gateway（独立 provider namespace）"},{"path":"src-tauri/src/proxy/handlers.rs","start_line":297,"end_line":306,"snippet":"        .collect()\n        .await\n        .map_err(|e| ProxyError::Internal(format!(\"Failed to read request body: {e}\")))?\n        .to_bytes();\n    let mut body: Value = serde_json::from_slice(&body_bytes)\n        .map_err(|e| ProxyError::Internal(format!(\"Failed to parse request body: {e}\")))?;\n    let stack = match resolve_stack_target(&state, &app_type, &mut body) {\n        Ok(stack) => stack,\n        Err(rejected) => return Ok(*rejected),\n    };"},{"path":"src-tauri/src/proxy/handlers.rs","start_line":329,"end_line":347,"snippet":"        .and_then(|s| s.as_bool())\n        .unwrap_or(false);\n\n    // 转发请求\n    let forwarder = ctx.create_forwarder(&state);\n    let mut result = match forwarder\n        .forward_with_retry(\n            &app_type,\n            method,\n            endpoint,\n            body.clone(),\n            headers,\n            extensions,\n            ctx.get_providers(),\n        )\n        .await\n    {\n        Ok(result) => result,\n        Err(mut err) => {"},{"path":"src-tauri/src/proxy/providers/codex.rs","start_line":1162,"end_line":1181,"snippet":"    fn get_auth_headers(\n        &self,\n        auth: &AuthInfo,\n    ) -> Result<Vec<(http::HeaderName, http::HeaderValue)>, ProxyError> {\n        use super::adapter::auth_header_value;\n        if auth.strategy == AuthStrategy::GitHubCopilot {\n            return super::copilot_auth::build_copilot_request_headers(&auth.api_key);\n        }\n        let bearer = format!(\"Bearer {}\", auth.api_key);\n        // Anthropic gateway: send only x-api-key (anthropic-version is filled in by\n        // the forwarder). Mutually exclusive with Bearer to avoid a 401 from the\n        // gateway receiving both auth headers at once.\n        if auth.strategy == AuthStrategy::Anthropic {\n            return Ok(vec![(\n                http::HeaderName::from_static(\"x-api-key\"),\n                auth_header_value(&auth.api_key)?,\n            )]);\n        }\n        Ok(vec![(\n            http::HeaderName::from_static(\"authorization\"),"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0013","rule_version":"1.1.0","rule_name":"Tool Output Sanitization & Injection Containment","finding":"NOT_APPLICABLE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"cc-switch is not an MCP server or agent framework that invokes tools and injects their return values into an LLM's system/developer prompt context. It is a transparent HTTP reverse-proxy/passthrough between a locally-running CLI (Claude Code, Codex, etc.) and the configured upstream provider API -- request/response bodies are forwarded (with format adaptation between provider API dialects) but cc-switch itself does not originate tool calls, execute tools, or construct LLM prompts/context. The rule's threat model (tool-output-to-prompt injection containment) does not have an applicable analog in this component's architecture.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"Tool outputs are a primary prompt-injection vector. Third-party content (web pages, files, API responses) may contain text crafted to override instructions or impersonate system/tool messages. Without a host-enforced envelope, sanitization, and an explicit untrusted-return signal, the model can be coerced into executing attacker-controlled directives, leaking secrets, or chaining further tool calls on behalf of the adversary.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":null,"evidence":[],"affected_tools":null,"component_name":null},{"rule_id":"ATR-C0014","rule_version":"1.2.0","rule_name":"CI/CD Workflow Script Injection","finding":"SECURE","severity":"HIGH","realized_severity":null,"confidence":"medium","rationale":"Reviewed all .github/workflows/*.yml run: blocks for direct interpolation of attacker-controlled ${{ github.event.* }} expressions (the classic script-injection pattern). None of the run: steps embed untrusted event fields directly; the claude.yml workflow passes PR/issue author-association gated conditions only in the `if:` trigger expression (not a shell context) and forwards free-text comment bodies only as a system-prompt argument to the claude-code-action tool (not executed as shell), with explicit `--disallowedTools Edit,Write,MultiEdit,NotebookEdit` and 'Never modify files, push commits, or create PRs' to constrain the agent. Semgrep's 34 'mutable-action-tag' findings (pinning actions by tag rather than SHA) are a supply-chain hardening gap, not an injection vulnerability, and are lower severity than this rule's injection threat model.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.","context_note":null,"within_declared_contract":null,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":".github/workflows/claude.yml","start_line":16,"end_line":34,"snippet":"      ((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') &&\n        (github.event.comment.author_association == 'OWNER' ||\n        (github.event.comment.author_association == 'OWNER' ...)))\n         github.event.comment.author_association == 'COLLABORATOR'))\n       ||\n       (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') &&\n        (github.event.comment.author_association == 'OWNER' ||\n         github.event.comment.author_association == 'MEMBER' ||\n         github.event.comment.author_association == 'COLLABORATOR'))"},{"path":".github/workflows/claude.yml","start_line":47,"end_line":54,"snippet":"      - name: Run Claude (review only)\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          claude_args: >-\n            --model claude-opus-4-7\n            --disallowedTools Edit,Write,MultiEdit,NotebookEdit"}],"affected_tools":null,"component_name":null},{"rule_id":"ATR-R0001","rule_version":"1.3.0","rule_name":"Weaponization: Malicious-Purpose Artifact (Offensive Capability by Design)","finding":"SECURE","severity":"CRITICAL","realized_severity":null,"confidence":"high","rationale":"cc-switch's declared and actual purpose (per README.md, package.json, and the entire codebase) is a legitimate developer productivity tool: switching between AI coding CLI provider configurations (Claude Code, Codex, Gemini CLI, etc.), managing MCP server/skill/prompt configs, and local proxying of API traffic between a developer's own tools and their own configured providers. No component advertises, implements, or instructs generation of malware, exploits, credential-harvesting, mass-abuse, or any offensive capability. The single YARA hit (Zeus_Agent_Remote_Bootstrap_Execution_WideSpan, confidence 25, described by the scanner itself as 'likely coincidental co-occurrence, not a pipeline') was manually reviewed in misc.rs lines 373-608: the flagged 'download'/'pipe_shell'/'quiet' identifiers are legitimate, user-initiated CLI tool installer plumbing (e.g. running the official `claude`/`codex`/`hermes` CLI install scripts via curl-to-tempfile-then-bash, explicitly NOT `curl | bash` per in-code comments, gated behind the VALID_TOOLS allowlist and an explicit user-triggered 'install/update' action) -- not a remote-bootstrap attack primitive serving an attacker.","vulnerability_details":null,"fix_guidance":null,"why_this_matters":"An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.","context_note":null,"within_declared_contract":true,"transport_gated":false,"mitigation_present":true,"evidence":[{"path":"src-tauri/src/commands/misc.rs","start_line":598,"end_line":613,"snippet":"/// 官方 shell installer 都不用 `curl | bash` 这种 pipe 形式(仍然用 curl 下载,\n/// 只是先落到临时文件再交给 bash 执行)\nconst CLAUDE_INSTALL_UNIX: &str =\n    \"bash -c 'tmp=$(mktemp) && curl -fsSL https://claude.ai/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'\";","vulnerability":{}},{"path":"src-tauri/src/commands/misc.rs","start_line":297,"end_line":331,"snippet":"pub async fn run_tool_lifecycle_action(\n    tools: Vec<String>,\n    action: String,\n    ...\n) -> Result<(), String> {\n    let requested = normalize_requested_tools(&tools);\n    if requested.is_empty() { return Err(\"No supported tools selected\".to_string()); }\n    ...","vulnerability":{}}],"affected_tools":null,"component_name":null}],"component_tools":[{"tool_name":"delete_mcp_server / delete_mcp_server_in_config","tool_description":"Deletes an MCP server configuration entry from the DB and from all downstream tool live configs","types":["destructive","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"import_mcp_from_apps / import_mcp_from_deeplink","tool_description":"Imports MCP server definitions from existing downstream tool configs or from a ccswitch:// deep link (base64-encoded JSON)","types":["write"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"open_external","tool_description":"Opens an external URL via the OS opener after validating scheme (http/https/mailto only)","types":["network-broad"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"resync_mcp_to_apps","tool_description":"Re-projects enabled MCP server configs from the DB into the live config files of the selected downstream apps","types":["write","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"run_tool_lifecycle_action (install/update CLI tool)","tool_description":"Tauri IPC command that builds and executes a shell/batch script to install or update a named CLI tool (claude/codex/gemini/grok/opencode/hermes/pi/mcode) via official installers or npm, downloading from hardcoded vendor URLs","types":["exec-capable","write"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"start_proxy_server / stop_proxy_server / stop_proxy_with_restore","tool_description":"Starts/stops the local HTTP reverse-proxy server (axum) used for provider request routing/failover; binds to 127.0.0.1 by default","types":["admin-config","network-broad"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"upsert_claude_mcp_server / delete_claude_mcp_server","tool_description":"Directly reads/writes the mcpServers map in ~/.claude.json","types":["write","destructive"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"upsert_mcp_server / upsert_mcp_server_in_config","tool_description":"Adds or updates an MCP server configuration entry in cc-switch's local DB and projects it into the live config of the selected downstream AI CLI tool","types":["write","admin-config"],"status":"IMPLEMENTED","component_name":"mcp-server"},{"tool_name":"validate_mcp_command","tool_description":"Checks whether a given command name is resolvable in PATH, without executing it","types":["read-only"],"status":"IMPLEMENTED","component_name":"mcp-server"}],"summary":{"high_level_risks":["Known HIGH-severity CVEs in transitively-pulled Rust dependencies (aws-lc-sys, openssl, quinn-proto) and a pnpm dependency (smol-toml), per dependency/supply-chain risk.","The component's real attack surface includes: (1) a local HTTP reverse-proxy (axum) bound to 127.0.0.1 by default (user-configurable listen_address) used to translate between LLM provider wire formats; (2) Tauri IPC commands that execute installer scripts (curl-to-tempfile-then-bash) for 9 allowlisted external CLI tools, downloading from hardcoded vendor HTTPS URLs; (3) an open_external command with an explicit scheme allowlist (http/https/mailto only); (4) a minisign-signed auto-updater fetching from two HTTPS endpoints."],"top_remediations":["Bump/patch aws-lc-sys, openssl, and quinn-proto (transitively pulled via reqwest's default native-tls feature) and smol-toml (pnpm) to versions without the flagged CVEs; consider setting reqwest default-features=false to drop the unused native-tls/quinn stack if not needed.","Consider scoping future analysis of this repo toward its actual security-relevant surfaces (local proxy auth/bind-address handling, Tauri capability/permission manifest, installer script construction, deep-link import validation) using a ruleset tailored to Tauri desktop apps.","Continue avoiding curl|bash style installer piping (already followed via download-to-tempfile-then-execute pattern) for all current and future CLI tool installers.","If cc-switch's in-process IPC commands are ever exposed via a remote transport in future (e.g. a companion MCP bridge), apply OAuth 2.1+PKCE, strict CORS/origin allowlisting, and TLS per the MCP Authorization spec before doing so."]},"rollup":{"component_findings_count":{"vulnerable":0,"secure":0,"not_applicable":11},"repo_findings_inherited_count":{"vulnerable":3,"secure":10,"not_applicable":2},"component_tools_count":{"destructive":2,"admin_config":4,"write":5,"network_broad":2,"exec_capable":1,"read_only":1},"worst_case_severity":"HIGH"},"trust_scores":{"repo_level":{"score":69,"tier":"medium","capability_band":"extreme","score_confidence":"low","score_interval":{"low":49,"high":89},"factors":["ATR-C0004 MEDIUM","ATR-C0007 HIGH","ATR-C0012 MEDIUM"]},"component_level":{"score":85,"tier":"high","capability_band":"extreme","score_confidence":"low","score_interval":{"low":62,"high":100},"factors":[]}}}]}