{"record":{"id":"d1d3606739a85c00","repo":"Hmbown/CodeWhale","slug":"reviewed-plugin-mcp-endpoint-has-an-unsafe-origin","errorCode":null,"errorMessage":"reviewed plugin MCP endpoint has an unsafe origin","messagePattern":"reviewed plugin MCP endpoint has an unsafe origin","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":1117,"sourceCode":"        .context(\"open reviewed launch file without links, hard links, or write/delete sharing\")\n}\n\n#[cfg(all(not(unix), not(windows)))]\nfn open_reviewed_launch_file(path: &Path) -> Result<fs::File> {\n    fs::File::open(path).context(\"open reviewed launch file\")\n}\n\nfn reviewed_remote_endpoint_identity(endpoint: &str) -> Result<(String, String)> {\n    let endpoint =\n        reqwest::Url::parse(endpoint).context(\"reviewed plugin MCP endpoint is invalid\")?;\n    if !endpoint.username().is_empty() || endpoint.password().is_some() {\n        anyhow::bail!(\"reviewed plugin MCP endpoint must not contain user information\");\n    }\n    if endpoint.query().is_some() || endpoint.fragment().is_some() {\n        anyhow::bail!(\"reviewed plugin MCP endpoint must not contain a query or fragment\");\n    }\n    let origin = reviewed_remote_origin(&endpoint)\n        .ok_or_else(|| anyhow::anyhow!(\"reviewed plugin MCP endpoint has an unsafe origin\"))?;\n    Ok((endpoint.to_string(), origin))\n}\n\nfn reviewed_remote_origin(endpoint: &reqwest::Url) -> Option<String> {\n    if !endpoint.username().is_empty() || endpoint.password().is_some() {\n        return None;\n    }\n    let host = endpoint.host_str()?;\n    let allowed_scheme = endpoint.scheme() == \"https\"\n        || (endpoint.scheme() == \"http\"\n            && (host.eq_ignore_ascii_case(\"localhost\")\n                || host\n                    .trim_matches(['[', ']'])\n                    .parse::<std::net::IpAddr>()\n                    .is_ok_and(|address| address.is_loopback())));\n    allowed_scheme.then(|| endpoint.origin().ascii_serialization())\n}\n","sourceCodeStart":1099,"sourceCodeEnd":1135,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp.rs#L1099-L1135","documentation":"For remote (HTTP/SSE) MCP endpoints from reviewed plugins, the endpoint URL is sanitized: no credentials (userinfo), no query string or fragment, and finally reviewed_remote_origin must yield a safe origin (scheme/host/port without embedded userinfo). If the URL has a scheme/host combination the origin extractor cannot consider safe, this error is returned instead of trusting the endpoint.","triggerScenarios":"Configuring a reviewed plugin's remote MCP endpoint with a URL whose origin cannot be safely derived — non-http(s) scheme, missing host, or (per reviewed_remote_origin) embedded username/password; also reached after the earlier checks for query/fragment pass.","commonSituations":"Typo'd scheme (ftp://, file://, ws://) in plugin config, a URL like https://user:token@host/mcp carrying credentials, empty/relative URL after template substitution, or an unresolvable/odd host form the URL parser produces no clean origin for.","solutions":["Use a plain https:// (or http:// where allowed) URL with an explicit host and no credentials, query, or fragment: https://mcp.example.com/mcp.","Move authentication out of the URL into headers/token config instead of user:pass@ userinfo.","Fix the scheme — file://, ws://, and custom schemes are rejected.","Validate the endpoint URL at plugin-review time so this never surfaces at connect time."],"exampleFix":"// before (plugin manifest)\nendpoint = \"https://user:secret@mcp.example.com/rpc?env=prod\"\n// after\nendpoint = \"https://mcp.example.com/rpc\"  # auth via headers","handlingStrategy":"validation","validationCode":"// reject unsafe endpoints at config load\nfn endpoint_ok(raw: &str) -> bool {\n    reqwest::Url::parse(raw).map(|u| {\n        matches!(u.scheme(), \"http\" | \"https\")\n            && u.host_str().map_or(false, |h| !h.is_empty())\n            && u.username().is_empty()\n            && u.password().is_none()\n            && u.query().is_none()\n            && u.fragment().is_none()\n    }).unwrap_or(false)\n}","typeGuard":"fn is_safe_origin(u: &reqwest::Url) -> bool {\n    crate::mcp::reviewed_remote_origin(u).is_some()\n}","tryCatchPattern":"let (endpoint, origin) = sanitize_endpoint(raw).map_err(|e| {\n    ConfigError::new(format!(\"plugin remote endpoint rejected: {e}; use a bare https://host/path URL\"))\n})?;","preventionTips":["Always use plain https://host/path endpoints for remote MCP servers","Keep credentials in headers or env, never in the URL","Strip query strings and fragments from endpoint config at review time","Validate endpoint URLs when the plugin is reviewed, not at connect time"],"tags":["mcp","url","security","ssrf","validation"],"backgroundTag":"invalid-url-format","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}