diff --git a/apps/desktop/src-tauri/src/extensions.rs b/apps/desktop/src-tauri/src/extensions.rs index 81817a85..9d1714ad 100644 --- a/apps/desktop/src-tauri/src/extensions.rs +++ b/apps/desktop/src-tauri/src/extensions.rs @@ -52,17 +52,43 @@ fn repo_to_raw_base(repo_url: &str, branch: &str, subpath: Option<&str>) -> Stri #[tauri::command] pub async fn get_registry_catalog(url: String) -> Result { - if url.starts_with("http://") || url.starts_with("https://") { + // Trim and lower-case the scheme before matching so inputs like + // ` HTTP://…` or `Https://…` still hit the intended branch instead of + // falling through to the local-file fallback with a confusing error. + let trimmed = url.trim(); + let scheme_prefix: String = trimmed.chars().take_while(|c| *c != ':').collect(); + let scheme_lc = scheme_prefix.to_ascii_lowercase(); + + // Plain `http://` (any casing/whitespace) is rejected outright: a MITM on + // the catalog can inject arbitrary `repository`/`data` entries that + // `install_extension` will then pass to `git clone`, escalating a network + // attack into code execution. + if scheme_lc == "http" { + return Err("Registry URL must use https://; plain HTTP is rejected to prevent MITM tampering of the catalog".to_string()); + } + + if scheme_lc == "https" { let response = shared_client() - .get(&url) + .get(trimmed) .timeout(DEFAULT_REQUEST_TIMEOUT) .send() .await .map_err(|e| e.to_string())?; + // Surface the HTTP failure explicitly, otherwise a 404/500 HTML body + // turns into a "Failed to parse registry JSON" error that hides the + // real problem (typo'd URL, registry missing, proxy blocking, …). + let status = response.status(); + if !status.is_success() { + return Err(format!( + "Failed to fetch registry from {}: HTTP {}", + trimmed, status + )); + } + let body = read_text_capped(response, MAX_RESPONSE_BYTES).await?; let catalog: RegistryCatalog = serde_json::from_str(&body).map_err(|e| { - let err = format!("Failed to parse registry JSON from {}: {}", url, e); + let err = format!("Failed to parse registry JSON from {}: {}", trimmed, e); error!("{}", err); redact_user_paths(&err) })?; @@ -70,7 +96,10 @@ pub async fn get_registry_catalog(url: String) -> Result = ({ chi setError(null); try { // The Tauri process CWD is usually apps/desktop/src-tauri, so the repo root is ../../../ - const registryUrl = "http://raw.githubusercontent.com/TrixtyAI/ide/main/registry/marketplace.json"; + const registryUrl = "https://raw.githubusercontent.com/TrixtyAI/ide/main/registry/marketplace.json"; const devRegistryUrl = "../../../registry/marketplace.json"; - // We will try devRegistryUrl, if it fails because it's a prod build, fallback to HTTP or viceversa. - // But for local test, let's just pass devRegistryUrl directly. const targetUrl = process.env.NODE_ENV === "development" ? devRegistryUrl : registryUrl; // 1. Fetch remote catalog raw list