{"record":{"id":"f9b59898501c6ab0","repo":"tinyhumansai/openhuman","slug":"debug-composio-base-url-must-be-https-or-loopback","errorCode":null,"errorMessage":"debug Composio base URL must be HTTPS or loopback HTTP","messagePattern":"debug Composio base URL must be HTTPS or loopback HTTP","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/tools/direct.rs","lineNumber":110,"sourceCode":"\n    pub(crate) fn auth_key_fingerprint(&self) -> u64 {\n        crate::openhuman::integrations::composio::direct_auth::fingerprint_api_key(&self.api_key)\n    }\n\n    /// Debug-test seam for raw integration coverage: construct a direct\n    /// Composio tool against explicit v2/v3 base URLs. Non-HTTPS URLs are\n    /// accepted only for loopback hosts and only in debug builds.\n    #[cfg(debug_assertions)]\n    pub fn new_with_base_urls_for_loopback(\n        api_key: &str,\n        default_entity_id: Option<&str>,\n        security: Arc<SecurityPolicy>,\n        base_v2: String,\n        base_v3: String,\n    ) -> anyhow::Result<Self> {\n        for base in [&base_v2, &base_v3] {\n            if !base.starts_with(\"https://\") && !is_loopback_http_base(base) {\n                anyhow::bail!(\"debug Composio base URL must be HTTPS or loopback HTTP\");\n            }\n        }\n        Ok(Self::new_internal(\n            api_key,\n            default_entity_id,\n            security,\n            base_v2,\n            base_v3,\n            true,\n        ))\n    }\n\n    /// Test-only seam: construct with an explicit Composio v3 base URL so\n    /// unit tests can point the direct `/tools` request — including the\n    /// `tags` filter — at a local mock instead of `backend.composio.dev`.\n    ///\n    /// `#[cfg(test)]`-gated on purpose: `list_tool_schemas_v3` attaches the\n    /// `x-api-key` header to whatever `base_v3` holds, so the only way to","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/tools/direct.rs#L92-L128","documentation":"Constructor validation in the debug-only ComposioTool::new_with_base_urls_for_loopback: each of base_v2/base_v3 must either start with https:// or be an http URL whose parsed host is loopback (localhost, 127.0.0.0/8, ::1) with no embedded userinfo. Host checking is done by parsing (is_loopback_http_base), not prefix matching, specifically so userinfo smuggling like http://127.0.0.1:8080@evil.com cannot route the x-api-key header to evil.com.","triggerScenarios":"A debug build constructs the tool with a base such as http://staging.composio.dev/... or http://192.168.1.10:8080 (LAN host, not loopback), or a loopback-looking URL containing user:pass@ — is_loopback_http_base returns false and the constructor bails.","commonSituations":"Pointing tests at a mock on a LAN IP or docker host (host.docker.internal, 172.17.x.x) instead of a loopback address; reusing a staging http URL in a dev build; copy-pasting a proxy URL with credentials.","solutions":["Point the base at a loopback host: http://127.0.0.1:<port>, http://localhost:<port>, or http://[::1]:<port>, with no userinfo","Or serve the mock over https and keep any host you like","Remember the constructor only exists under cfg(debug_assertions) — release builds must use ComposioTool::new with the pinned HTTPS endpoints"],"exampleFix":"// before\nlet tool = ComposioTool::new_with_base_urls_for_loopback(key, None, sec,\n    \"http://10.0.0.5:8080/api/v2\".into(), \"http://10.0.0.5:8080/api/v3\".into())?;\n\n// after — loopback mock in a debug build\nlet tool = ComposioTool::new_with_base_urls_for_loopback(key, None, sec,\n    \"http://127.0.0.1:8080/api/v2\".into(), \"http://127.0.0.1:8080/api/v3\".into())?;","handlingStrategy":"validation","validationCode":"#[cfg(debug_assertions)]\n{\n    // Both bases must be https OR loopback http (127.0.0.1 / localhost / [::1], no userinfo)\n    for base in [&base_v2, &base_v3] {\n        assert!(base.starts_with(\"https://\") || is_loopback_http_base(base),\n            \"base {base} is neither https nor loopback\");\n    }\n}","typeGuard":"fn is_safe_debug_base(raw: &str) -> bool {\n    if raw.starts_with(\"https://\") { return true; }\n    let Ok(u) = url::Url::parse(&format!(\"{}/\", raw.trim_end_matches('/'))) else { return false };\n    u.scheme() == \"http\"\n        && u.username().is_empty()\n        && u.password().is_none()\n        && matches!(u.host(),\n            Some(url::Host::Domain(h)) if h.eq_ignore_ascii_case(\"localhost\")\n            || matches!(u.host(), Some(url::Host::Ipv4(ip)) if ip.is_loopback())\n            || matches!(u.host(), Some(url::Host::Ipv6(ip)) if ip.is_loopback()))\n}","tryCatchPattern":"match ComposioTool::new_with_base_urls_for_loopback(key, None, sec, v2, v3) {\n    Ok(tool) => tool,\n    Err(e) if format!(\"{e:#}\").contains(\"HTTPS or loopback\") => {\n        // test harness misconfig — bind the mock to 127.0.0.1 or serve TLS\n        panic!(\"test base URL rejected: {e:#}\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Bind local Composio mocks to 127.0.0.1 / [::1] / localhost — never a LAN or docker-bridge IP — when using the debug constructor","Never put credentials in mock base URLs; the loopback check rejects userinfo by design","Gate every use of new_with_base_urls_for_loopback behind cfg(debug_assertions) so it cannot leak into release builds"],"tags":["composio","security","loopback","debug-build","url-scheme","constructor-validation"],"backgroundTag":"insecure-url-scheme-blocked","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}