farion1231/cc-switch

MCP server

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

farion1231Rust, TypeScript144,367 starsScanned Oct. 14th, 2026at 51ac86fView source

Trust score

69/100Moderate trust
Likely 49–89 · low confidence

Scanned Oct. 14th, 2026 at 51ac86f against 26 rules · How scoring works

For developersOpen rawGET /c/farion1231/cc-switch/report.json
At a glance

At a glance

Contains
1 MCP server · 9 tools
Its tools can
Run code or commands 1Delete or overwrite data 2Change settings 4Reach any network address 2Create or change data 5Read data 1
Issues flagged
1 high2 medium3 of 26 rule checks, severity as assessed for this repo
Where issues are
  • Repository-wide3 of 15
No issues in the other 1 of 2.
Trust signals
Publisher not verifiedUnsigned build31 known CVEs in dependenciesActively maintained

What to fix first

3 issues, most severe first. Each one points to the exact code, with the recommended fix and a prompt you can paste into your coding agent.

  1. 1HighDependency Risk & Supply Chain
    Repository-wide · ATR-C0007
    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.
    [[package]]
    name = "aws-lc-sys"
    version = "0.36.0"
    Show 2 more locations
    reqwest = { version = "0.12", features = ["rustls-tls", "json", "stream", "socks"] }
    name = "reqwest"
    ...
    dependencies = [
     ...
     "hyper-rustls",
     "hyper-tls",
     ...
     "native-tls",
     ...
     "quinn",
     "rustls",
     ...
    ]

    +3 more locations in the finding JSON

    Recommended fix

    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.

  2. 2MediumDoS Protections: Timeouts, Concurrency, Streaming Limits
    Repository-wide · ATR-C0004
    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.
            if self.shutdown_tx.read().await.is_some() {
                return Err(ProxyError::AlreadyRunning);
            }
    
            let addr: SocketAddr =
                format!("{}:{}", self.config.listen_address, self.config.listen_port)
                    .parse()
                    .map_err(|e| ProxyError::BindFailed(format!("无效的地址: {e}")))?;
    
            // 创建关闭通道
            let (shutdown_tx, shutdown_rx) = oneshot::channel();
    
            // 构建路由
            let app = self.build_router();
    …
    Show 2 more locations
                loop {
                    tokio::select! {
                        result = listener.accept() => {
                            let (stream, _remote_addr) = match result {
                                Ok(v) => v,
                                Err(e) => {
                                    log::error!("[{SRV}] accept 失败: {e}", SRV = log_srv::ACCEPT_ERR);
                                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
                                    continue;
                                }
                            };
    
                            let app = app.clone();
                            tokio::spawn(async move {
    …
                .route("/gemini/v1beta/*path", any(handlers::handle_gemini))
                // Gemini 的 GA 版本也叫 /v1,给原 SDK 留一条出口
                .route("/gemini/v1/*path", any(handlers::handle_gemini))
                // 提高默认请求体大小限制(避免 413 Payload Too Large)
                .layer(DefaultBodyLimit::max(200 * 1024 * 1024))
                .with_state(self.state.clone())
        }
    Recommended fix

    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.

  3. 3MediumDenial of Wallet / Cost Controls
    Repository-wide · ATR-C0012
    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.
            if self.shutdown_tx.read().await.is_some() {
                return Err(ProxyError::AlreadyRunning);
            }
    
            let addr: SocketAddr =
                format!("{}:{}", self.config.listen_address, self.config.listen_port)
                    .parse()
                    .map_err(|e| ProxyError::BindFailed(format!("无效的地址: {e}")))?;
    
            // 创建关闭通道
            let (shutdown_tx, shutdown_rx) = oneshot::channel();
    
            // 构建路由
            let app = self.build_router();
    …
    Show 2 more locations
    
        fn build_router(&self) -> Router {
            Router::new()
                // 健康检查
                .route("/health", get(handlers::health_check))
                .route("/status", get(handlers::get_status))
                // Claude API (支持带前缀和不带前缀两种格式)
                .route("/v1/messages", post(handlers::handle_messages))
                .route("/claude/v1/messages", post(handlers::handle_messages))
                // Claude Desktop 3P 本地 gateway(独立 provider namespace)
            .collect()
            .await
            .map_err(|e| ProxyError::Internal(format!("Failed to read request body: {e}")))?
            .to_bytes();
        let mut body: Value = serde_json::from_slice(&body_bytes)
            .map_err(|e| ProxyError::Internal(format!("Failed to parse request body: {e}")))?;
        let stack = match resolve_stack_target(&state, &app_type, &mut body) {
            Ok(stack) => stack,
            Err(rejected) => return Ok(*rejected),
        };

    +2 more locations in the finding JSON

    Recommended fix

    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.

What’s inside

BlueRock found 1 component in this repository. Each is checked against the rules for its type.

MCP servermcp-server.85High trustNone flagged

Its tools can

Run code or commands 1Delete or overwrite data 2Change settings 4Reach any network address 2Create or change data 5Read data 1

What stood out

  • 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.

MCP server rules 0 of 11 flagged

11 did not applyShow
Does not applyOAuth 2.1 + PKCE Correctness (MCP Authorization)ATR-M0005Details

Why it got this result

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.

Why it matters

Misconfig enables token theft and confused-deputy attacks.

Evidence

//! Codex OAuth Tauri Commands
//!
//! 提供 OpenAI ChatGPT Plus/Pro OAuth 认证相关的 Tauri 命令。
//! ... Codex OAuth 专属的订阅额度和模型列表查询命令。
pub struct CodexOAuthState(pub Arc<CodexOAuthManager>);
Does not applyNo Authentication or Authorization Checks (General)ATR-M0010Details

Why it got this result

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.

Why it matters

Prevents unauthorized tool use and data access.

Evidence

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "enables the default permissions",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default",
    "updater:default",
    "log:default",
...
Does not applyOAuth Proxy Per-Client Consent (Confused Deputy Defense)ATR-M0011Details

Why it got this result

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.

Why it 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.

Evidence

pub struct CodexOAuthState(pub Arc<CodexOAuthManager>);

/// 查询 Codex OAuth (ChatGPT Plus/Pro) 订阅额度
Does not applyTool Definition Integrity & Versioning (ETDI-style)ATR-M0001Details

Why it got this result

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.'

Why it matters

Prevents rug-pull tool poisoning.

Evidence

"updater": {
  "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM4MDI4QzlBNTczOTI4RTMKUldUaktEbFhtb3dDeUM5US9kT0FmdGR5Ti9vQzcwa2dTMlpibDVDUmQ2M0VGTzVOWnd0SGpFVlEK",
  "endpoints": [
    "https://dl.ccswitch.io/latest.json",
    "https://github.com/farion1231/cc-switch/releases/latest/download/latest.json"
  ]
}
Does not applyUnrestricted Tool Permissions (Overprivileged Tools)ATR-M0002Details

Why it got this result

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.

Why it matters

Limits blast radius and privilege escalation.

Evidence

const VALID_TOOLS: [&str; 9] = [
    "claude", "codex", "gemini", "grok", "opencode", "openclaw", "hermes", "pi", "mcode",
];
Show 1 more location
    if !VALID_TOOLS.contains(&tool.as_str()) {
        return Err(format!("Unsupported tool action target: {tool}"));
    }
Does not applyCORS / Origin / Host Validation (DNS Rebinding Defense)ATR-M0003Details

Why it got this result

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.

Why it matters

Prevents DNS rebinding/browser hijack of local or remote MCP.

Evidence

impl Default for ProxyConfig {
    fn default() -> Self {
        Self {
            listen_address: "127.0.0.1".to_string(),
            listen_port: 15721, // 使用较少占用的高位端口
            max_retries: 3,
            request_timeout: 600,
            enable_logging: true,
...
Does not applyBinding to 0.0.0.0 Without Auth/TLSATR-M0004Details

Why it got this result

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.

Why it matters

LAN/Internet exposure enables remote exploitation.

Evidence

impl Default for ProxyConfig {
    fn default() -> Self {
        Self {
            listen_address: "127.0.0.1".to_string(),
            listen_port: 15721,
...
Does not applyCORS Misuse: Wildcard with CredentialsATR-M0008Details

Why it got this result

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.

Why it matters

Prevents credentialed cross-origin abuse.

Evidence

let app = Router::new()
    .route("/health", get(handlers::health_check))
    .route("/status", get(handlers::get_status))
    .route("/v1/messages", post(handlers::handle_messages))
    .route("/claude/v1/messages", post(handlers::handle_messages))
...
Does not applyTransport Security: TLS Everywhere (Remote)ATR-M0009Details

Why it got this result

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.

Why it matters

MitM can steal tokens and tamper responses.

Evidence

impl Default for ProxyConfig {
    fn default() -> Self {
        Self {
            listen_address: "127.0.0.1".to_string(),
            listen_port: 15721,
...
Does not applyTool Name Collision / SpoofingATR-M0006Details

Why it got this result

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.

Why it matters

Prevents malicious tool shadowing.

Evidence

        .invoke_handler(tauri::generate_handler![
Does not applyPrompt/Metadata Injection in Tool DescriptionsATR-M0007Details

Why it got this result

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.

Why it matters

Prevents covert model manipulation.

Evidence

/// 获取 Claude MCP 状态
#[tauri::command]
pub async fn get_claude_mcp_status() -> Result<claude_mcp::McpStatus, String> {
    claude_mcp::get_mcp_status().map_err(|e| e.to_string())
}
Show its 9 tools
ToolIt can
delete_mcp_server / delete_mcp_server_in_config
Deletes an MCP server configuration entry from the DB and from all downstream tool live configs
Delete or overwrite dataChange settings
import_mcp_from_apps / import_mcp_from_deeplink
Imports MCP server definitions from existing downstream tool configs or from a ccswitch:// deep link (base64-encoded JSON)
Create or change data
open_external
Opens an external URL via the OS opener after validating scheme (http/https/mailto only)
Reach any network address
resync_mcp_to_apps
Re-projects enabled MCP server configs from the DB into the live config files of the selected downstream apps
Create or change dataChange settings
run_tool_lifecycle_action (install/update CLI tool)
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
Run code or commandsCreate or change data
start_proxy_server / stop_proxy_server / stop_proxy_with_restore
Starts/stops the local HTTP reverse-proxy server (axum) used for provider request routing/failover; binds to 127.0.0.1 by default
Change settingsReach any network address
upsert_claude_mcp_server / delete_claude_mcp_server
Directly reads/writes the mcpServers map in ~/.claude.json
Create or change dataDelete or overwrite data
upsert_mcp_server / upsert_mcp_server_in_config
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
Create or change dataChange settings
validate_mcp_command
Checks whether a given command name is resolvable in PATH, without executing it
Read data

Before you put it to work

  1. 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.
  2. 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.
  3. Continue avoiding curl|bash style installer piping (already followed via download-to-tempfile-then-execute pattern) for all current and future CLI tool installers.
  4. 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.

Repository-wide rules

Checked once for the whole repository: dependencies, secrets, inputs, networking, CI and more. 3 of 15 flagged.

10 rules passed · 2 did not applyShow
PassedRuntime Command Injection via Process ExecutionATR-C0003Details

Why it got this result

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.

Why it matters

Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.

Evidence

const VALID_TOOLS: [&str; 9] = [
    "claude", "codex", "gemini", "grok", "opencode", "openclaw", "hermes", "pi", "mcode",
];
Show 2 more locations
fn normalize_requested_tools(tools: &[String]) -> Vec<&'static str> {
    let set: std::collections::HashSet<&str> = tools.iter().map(|s| s.as_str()).collect();
    VALID_TOOLS
        .iter()
        .copied()
        .filter(|tool| set.contains(tool))
        .collect()
}
fn build_wsl_tool_action_line(distro: &str, command: &str, force_shell: Option<&str>, force_shell_flag: Option<&str>) -> Result<String, String> {
    if !is_valid_wsl_distro_name(distro) { return Err(...); }
    let shell = force_shell.map(...).unwrap_or("sh");
    if !is_valid_shell(shell) { return Err(...); }
    ...
    Ok(format!("wsl.exe -d {distro} -- {shell} {flag} {}", windows_cmd_double_quote_arg(command)))
}

+1 more location in the finding JSON

PassedWeaponization: Malicious-Purpose Artifact (Offensive Capability by Design)ATR-R0001Details

Why it got this result

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.

Why it 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.

Evidence

/// 官方 shell installer 都不用 `curl | bash` 这种 pipe 形式(仍然用 curl 下载,
/// 只是先落到临时文件再交给 bash 执行)
const CLAUDE_INSTALL_UNIX: &str =
    "bash -c 'tmp=$(mktemp) && curl -fsSL https://claude.ai/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
Show 1 more location
pub async fn run_tool_lifecycle_action(
    tools: Vec<String>,
    action: String,
    ...
) -> Result<(), String> {
    let requested = normalize_requested_tools(&tools);
    if requested.is_empty() { return Err("No supported tools selected".to_string()); }
    ...
PassedUnrestricted Network Fetch / SSRFATR-C0001Details

Why it got this result

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.

Why it matters

Enables internal pivoting and data exfiltration.

Evidence

/// 链接里的 `iconUrl` 形状是否可用:https、域名主机、默认端口、没有账号密码。
/// 解析阶段用它过滤,下载时每一跳重定向也再过一遍。
pub(crate) fn parse_icon_url(raw: &str) -> Option<Url> {
    let raw = raw.trim();
    ...
        return None;
    }
    let url = Url::parse(raw).ok()?;
    !(a == 0 || a == 10 || a == 127 || (a == 100 && (64..128).contains(&b)) ...)
        || !url.username().is_empty()
Show 1 more location
/// 解析域名,要求每个地址都是公网地址。有一个不是就整体拒绝,不挑着用。
    let addrs: Vec<SocketAddr> = tokio::net::lookup_host((domain.as_str(), 443)).await...;
    if let Some(addr) = addrs.iter().find(|addr| !is_public_ip(addr.ip())) { return Err(...); }
    ...
    };
    let domain = domain.to_string();
    ... .resolve_to_addrs(&domain, &addrs) ... .redirect(reqwest::redirect::Policy::none()) ...
PassedStatic SecretsATR-C0002Details

Why it got this result

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.

Why it matters

Plaintext secrets are easily exfiltrated.

Evidence

#[test]
    fn test_auth_info_with_access_token() {
        let auth = AuthInfo::with_access_token(
            "refresh-token".to_string(),
            "ya29.access-token-12345".to_string(),
        );
        assert_eq!(auth.api_key, "refresh-token");
        assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth);
Show 2 more locations
GEMINI_API_KEY": "[REDACTED]"  <!-- example payload shown in a static HTML usage-script generator, not a real credential -->

+19 more locations in the finding JSON

PassedMissing or Inadequate Input ValidationATR-C0008Details

Why it got this result

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.

Why it matters

Frontline control to prevent multiple classes of vulns.

Evidence

pub fn validate_url(url_str: &str, field_name: &str) -> Result<(), AppError> {
    let url = Url::parse(url_str)...;
        .map_err(|e| AppError::InvalidInput(format!("Invalid URL for '{field_name}': {e}")))?;
    if scheme != "http" && scheme != "https" { return Err(...); }
    let scheme = url.scheme();
    if scheme != "http" && scheme != "https" {
Show 1 more location
const ALLOWED_FILES: &[&str] = &["AGENTS.md", "SOUL.md", ...];
fn validate_filename(filename: &str) -> Result<(), String> {
    if !ALLOWED_FILES.contains(&filename) { return Err(...); }
    Ok(())
}
PassedInsecure DeserializationATR-C0009Details

Why it got this result

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.

Why it matters

Can lead to arbitrary code execution.

Evidence

// Parse JSON
    let config_json: Value = serde_json::from_str(&config_str)
        .map_err(|e| AppError::InvalidInput(format!("Invalid JSON in MCP config: {e}")))?;
PassedPath Traversal & Symlink Bypass (Filesystem Tools)ATR-C0011Details

Why it got this result

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.

Why it matters

Traversal exposes arbitrary host files; can lead to code execution.

Evidence

fn sanitize_skill_source_path(raw: &str) -> Option<PathBuf> {
    ...
    for component in Path::new(trimmed).components() {
        match component {
            Component::Normal(name) => { ... }
            Component::CurDir | Component::ParentDir | Component::RootDir | Component::Prefix(_) => { return None; }
        }
    }
Show 1 more location
// GitHub 归档统一带一层 `<repo>-<branch>/` 根目录,需剚掉后再落盘。
 let Ok(relative_path) = safe_path.strip_prefix(&root_name) else { continue; };
 // 第二道:enclosed_name() 的保证是相对归档根的,且它不规范化路径
 if relative_path.components().any(|c| matches!(c, Component::ParentDir)) {
     log::warn!("跳过越界的压缩包条目: {}", file.name());
     continue;
 }
PassedCI/CD Workflow Script InjectionATR-C0014Details

Why it got this result

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.

Why it 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.

Evidence

      ((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') &&
        (github.event.comment.author_association == 'OWNER' ||
        (github.event.comment.author_association == 'OWNER' ...)))
         github.event.comment.author_association == 'COLLABORATOR'))
       ||
       (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') &&
        (github.event.comment.author_association == 'OWNER' ||
         github.event.comment.author_association == 'MEMBER' ||
         github.event.comment.author_association == 'COLLABORATOR'))
Show 1 more location
      - name: Run Claude (review only)
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          claude_args: >-
            --model claude-opus-4-7
            --disallowedTools Edit,Write,MultiEdit,NotebookEdit
PassedLogging Hygiene (No Secrets/PII/Tokens) & Audit TrailATR-C0005Details

Why it got this result

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.

Why it matters

Prevents sensitive data leakage; enables forensics.

Evidence

/// reqwest 的错误文本自带完整 URL(含查询参数,可能是 CDN 签名或令牌),而重定向之后的
/// 地址也没经过 `url_for_log`。去掉 URL,只留错误类型和底层原因写进日志。
fn describe_request_error(error: reqwest::Error) -> String {
    let error = error.without_url();
    let mut text = error.to_string();
Show 1 more location
async fn request_errors_leave_the_url_out_of_the_log() {
        ... .get("http://127.0.0.1:9/logo.png?token=SECRET_TOKEN") ...
        assert!(error.to_string().contains("SECRET_TOKEN"));
        let described = describe_request_error(error);
        assert!(!described.contains("SECRET_TOKEN"), "{described}");
PassedLack of Output Encoding/SanitizationATR-C0010Details

Why it got this result

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.

Why it matters

Prevents UI-level injection and operator compromise.

Evidence

const iconSvg = useMemo(() => {
    if (icon && !isCustomIcon(icon) && !isUrlIcon(icon) && hasIcon(icon)) {
      return getIcon(icon);
    }
    return "";
  }, [icon]);
Show 1 more location
  if (iconSvg) {
    return (
      <span ... dangerouslySetInnerHTML={{ __html: iconSvg }} />
        className={cn(
          "inline-flex items-center justify-center flex-shrink-0",
          className,
        )}
      <img src={iconUrl} onError={...} alt={name} ... />
        style={{ ...sizeStyle, color: effectiveColor }}
        dangerouslySetInnerHTML={{ __html: iconSvg }}
Does not applyTool Output Sanitization & Injection ContainmentATR-C0013Details

Why it got this result

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.

Why it 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.

Does not applyContainer/Runtime HardeningATR-C0006Details

Why it got this result

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.

Why it matters

Reduces blast radius of exploits.

From build to runtime

You know what it is. Now see what it does.

A scan helps you understand what you’re building with. When your agent runs, BlueRock shows you what actually happens across tools and systems. Follow actions in context, understand behavior as it unfolds and add controls when you need them.

BUILDRegistry + scan
RUNSee actual execution
OPERATEObserve, control, optimize

Results reflect the code at the commit and date shown.