{"record":{"id":"77b97eee03fa5d0e","repo":"tinyhumansai/openhuman","slug":"refusing-to-transmit-sensitive-data-over-non-https","errorCode":null,"errorMessage":"Refusing to transmit sensitive data over non-HTTPS URL: URL scheme must be https","messagePattern":"Refusing to transmit sensitive data over non-HTTPS URL: URL scheme must be https","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/tools/direct.rs","lineNumber":24,"sourceCode":"// This is opt-in. Users who prefer sovereign/local-only mode skip this entirely.\n// The Composio API key is stored in the encrypted secret store.\n\nuse crate::openhuman::security::policy::ToolOperation;\nuse crate::openhuman::security::SecurityPolicy;\nuse crate::openhuman::tools::traits::{Tool, ToolCategory, ToolResult};\nuse anyhow::Context;\nuse async_trait::async_trait;\nuse reqwest::Client;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse std::sync::Arc;\n\nconst COMPOSIO_API_BASE_V2: &str = \"https://backend.composio.dev/api/v2\";\nconst COMPOSIO_API_BASE_V3: &str = \"https://backend.composio.dev/api/v3\";\n\nfn ensure_https(url: &str) -> anyhow::Result<()> {\n    if !url.starts_with(\"https://\") {\n        anyhow::bail!(\n            \"Refusing to transmit sensitive data over non-HTTPS URL: URL scheme must be https\"\n        );\n    }\n    Ok(())\n}\n\nfn is_loopback_http_url(url: &str) -> bool {\n    // Parse rather than prefix-match: a raw `starts_with(\"http://127.0.0.1:\")`\n    // is fooled by userinfo smuggling like\n    // `http://127.0.0.1:8080@evil.com/api/v3/tools`, which reqwest routes to the\n    // *parsed* host (`evil.com`). Verify the actual scheme + host and reject any\n    // embedded credentials so the insecure-loopback path can never leak the\n    // `x-api-key` header to a non-loopback host.\n    let Ok(parsed) = url::Url::parse(url) else {\n        return false;\n    };\n    if parsed.scheme() != \"http\" {\n        return false;","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/tools/direct.rs#L6-L42","documentation":"Security guard in the direct Composio tool: ensure_https refuses any URL that does not literally start with 'https://' before a request carrying the x-api-key header is transmitted. It exists so a mis-configured base URL (env var, injected config, hand-built string) can never leak the API key over plaintext HTTP. The only sanctioned exception is loopback HTTP in debug builds (new_with_base_urls_for_loopback / allow_insecure_loopback).","triggerScenarios":"ComposioTool is built with base URLs (or issues a request URL) using http:// — e.g. pointing v2/v3 bases at an http:// staging host in a release build — so ensure_request_url -> ensure_https bails before send(). Also fires for a non-lowercase 'HTTPS://' prefix because the check is a case-sensitive starts_with.","commonSituations":"Local mock server wired into a release build; base URL injected via config or env with a typo ('http://' instead of 'https://'); a proxy or config rewrite mangling the scheme; hand-concatenated URL strings with wrong casing.","solutions":["Use https:// base URLs for any non-loopback host — production pins https://backend.composio.dev/api/v2 and /api/v3","For local mock servers, use the debug-only new_with_base_urls_for_loopback constructor with a 127.0.0.1, ::1, or localhost host","Make sure the scheme string is lowercase 'https://' — the guard is a literal prefix match","Audit any env/config override of Composio base URLs before shipping"],"exampleFix":"// before\nlet base = std::env::var(\"COMPOSIO_BASE\")?; // \"http://composio-mirror.internal\"\n\n// after — enforce https for non-loopback hosts before constructing the tool\nif !base.starts_with(\"https://\") {\n    anyhow::bail!(\"COMPOSIO_BASE must use https (loopback http allowed only via the debug constructor)\");\n}","handlingStrategy":"validation","validationCode":"if !base.starts_with(\"https://\") {\n    anyhow::bail!(\"Composio base URL must be https:// (loopback http only via the debug constructor)\");\n}","typeGuard":"fn is_https_url(raw: &str) -> bool {\n    url::Url::parse(raw).map(|u| u.scheme() == \"https\").unwrap_or(false)\n}","tryCatchPattern":"match tool.list_actions(None).await {\n    Ok(items) => { /* ... */ }\n    Err(e) if format!(\"{e:#}\").contains(\"non-HTTPS\") => {\n        // configuration defect — fail loudly, never downgrade to http\n        return Err(e);\n    }\n    Err(e) => { /* normal API error handling */ }\n}","preventionTips":["Never accept Composio base URLs from config without asserting the https:// prefix","Use ComposioTool::new (pinned HTTPS endpoints) everywhere except debug loopback testing","Keep the scheme lowercase when building URL strings — the guard is a literal prefix match","Treat this error as a security stop, not an inconvenience: do not catch-and-continue"],"tags":["composio","security","https","url-scheme","tls","secret-leak-prevention"],"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"}