From b63c347721d49abf0e38255923ad08cef26a4809 Mon Sep 17 00:00:00 2001 From: Matias Palma Date: Mon, 20 Apr 2026 22:03:47 -0400 Subject: [PATCH 1/2] fix: force https for extension registry fetch to prevent MITM catalog tampering The prod registry URL was hardcoded to http://raw.githubusercontent.com, which allows a network attacker to inject arbitrary repository/data entries into the catalog. Those entries are then passed to install_extension and cloned via git, escalating a network MITM directly into code execution on the user's machine. - Frontend: switch the registry URL to https://. - Backend: reject http:// URLs in get_registry_catalog outright; only accept https:// or local file paths (dev mode). - Drop the stale comment about an HTTP/local fallback that never existed (the ternary always picked exactly one branch based on NODE_ENV). --- apps/desktop/src-tauri/src/extensions.rs | 9 ++++++++- apps/desktop/src/context/ExtensionContext.tsx | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/extensions.rs b/apps/desktop/src-tauri/src/extensions.rs index 81817a85..1be267bf 100644 --- a/apps/desktop/src-tauri/src/extensions.rs +++ b/apps/desktop/src-tauri/src/extensions.rs @@ -52,7 +52,14 @@ 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://") { + // Plain `http://` 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 url.starts_with("http://") { + return Err("Registry URL must use https://; plain HTTP is rejected to prevent MITM tampering of the catalog".to_string()); + } + + if url.starts_with("https://") { let response = shared_client() .get(&url) .timeout(DEFAULT_REQUEST_TIMEOUT) diff --git a/apps/desktop/src/context/ExtensionContext.tsx b/apps/desktop/src/context/ExtensionContext.tsx index 59f5d954..4bcae5ea 100644 --- a/apps/desktop/src/context/ExtensionContext.tsx +++ b/apps/desktop/src/context/ExtensionContext.tsx @@ -114,11 +114,9 @@ export const ExtensionProvider: React.FC<{ children: React.ReactNode }> = ({ 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 From ca70a2f06230d87e44c01f6a4b41ee7380ad101c Mon Sep 17 00:00:00 2001 From: Matias Palma Date: Mon, 20 Apr 2026 22:57:22 -0400 Subject: [PATCH 2/2] fix: normalize registry URL scheme check and surface non-2xx HTTP status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #162: - Trim whitespace and lower-case the scheme before matching so inputs like " HTTP://…" or "Https://…" hit the intended branch instead of silently falling through to the local-file fallback with a confusing "Failed to read local registry file" error. - Check response.status() after send(). A 404/500 HTML body was previously becoming "Failed to parse registry JSON", hiding the real HTTP failure (typo'd URL, registry missing, proxy blocking). --- apps/desktop/src-tauri/src/extensions.rs | 38 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/src/extensions.rs b/apps/desktop/src-tauri/src/extensions.rs index 1be267bf..9d1714ad 100644 --- a/apps/desktop/src-tauri/src/extensions.rs +++ b/apps/desktop/src-tauri/src/extensions.rs @@ -52,24 +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 { - // Plain `http://` 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 url.starts_with("http://") { + // 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 url.starts_with("https://") { + 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) })?; @@ -77,7 +96,10 @@ pub async fn get_registry_catalog(url: String) -> Result