From 5ce2c250bbf1f6647726666db7356bfeb6906aa6 Mon Sep 17 00:00:00 2001 From: Harald Date: Tue, 6 Oct 2026 19:45:16 +0200 Subject: [PATCH] =?UTF-8?q?fix(updater):=20HTTPS=20f=C3=BCr=20alle=20Anfra?= =?UTF-8?q?gen=20erzwingen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher wurde nur die Server-URL geprüft. Download-Links aus der Release-API und Weiterleitungsziele durften auch http:// sein, womit Paket und SHA256SUMS.txt unverschlüsselt geladen werden konnten. Jetzt wird jede Anfrage einzeln geprüft (allow_insecure_http bleibt als Ausnahme). Co-Authored-By: Claude Opus 5.5 --- src/updater.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/updater.rs b/src/updater.rs index 670ab53..687cdac 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -368,6 +368,17 @@ pub enum UpdaterStatus { Error(String), } +/// Lehnt jede URL ohne HTTPS ab, sofern unsicheres HTTP nicht ausdrücklich erlaubt ist. +fn ensure_secure_scheme(url: &url::Url, allow_insecure_http: bool) -> Result<(), String> { + match url.scheme() { + "https" => Ok(()), + "http" if allow_insecure_http => Ok(()), + scheme => Err(format!( + "Unsicheres Protokoll '{scheme}' für '{url}' abgewiesen. Es wird zwingend HTTPS verlangt." + )), + } +} + /// Erstellt einen ureq Agent mit sauberen Timeouts. fn create_http_agent() -> ureq::Agent { ureq::AgentBuilder::new() @@ -384,12 +395,15 @@ fn http_get_with_auth_guard( initial_url_str: &str, token: Option<&str>, max_redirects: usize, + allow_insecure_http: bool, ) -> Result { let mut current_url = url::Url::parse(initial_url_str) .map_err(|e| format!("Ungültige URL '{initial_url_str}': {e}"))?; let initial_origin = current_url.origin().ascii_serialization(); for _ in 0..=max_redirects { + // Jede Anfrage prüfen – auch Download-Links aus der API und Weiterleitungsziele + ensure_secure_scheme(¤t_url, allow_insecure_http)?; let mut req = agent.get(current_url.as_str()); // Token NUR anhängen, wenn Origin (Scheme, Host, Port) identisch ist @@ -500,7 +514,13 @@ impl UpdaterController { ); let agent = create_http_agent(); - match http_get_with_auth_guard(&agent, &api_url, config.auth_token.as_deref(), 5) { + match http_get_with_auth_guard( + &agent, + &api_url, + config.auth_token.as_deref(), + 5, + config.allow_insecure_http, + ) { Ok(resp) => match resp.into_json::() { Ok(release) => { if release.prerelease && !config.include_prereleases { @@ -569,6 +589,7 @@ impl UpdaterController { &asset.browser_download_url, config.auth_token.as_deref(), 5, + config.allow_insecure_http, ) { Ok(r) => r, Err(e) => { @@ -658,6 +679,7 @@ impl UpdaterController { &ca.browser_download_url, config.auth_token.as_deref(), 5, + config.allow_insecure_http, ) { Ok(c_resp) => match c_resp.into_string() { Ok(text) => text, @@ -996,6 +1018,30 @@ dcdeb7ac87d9e6cb8022e5a71bc7f2c56d779b4fce4efdd5bae5be3b0214884a ink-notes_0.1. assert!(allowed_insecure.validate_url().is_ok()); } + #[test] + fn test_https_enforced_for_every_request() { + let https = url::Url::parse("https://gitea.pansi.eu/a.deb").unwrap(); + let http = url::Url::parse("http://cdn.example.com/a.deb").unwrap(); + let ftp = url::Url::parse("ftp://example.com/a.deb").unwrap(); + assert!(ensure_secure_scheme(&https, false).is_ok()); + assert!(ensure_secure_scheme(&http, false).is_err()); + assert!(ensure_secure_scheme(&http, true).is_ok()); + assert!(ensure_secure_scheme(&ftp, true).is_err()); + + // Ein http-Download-Link aus der Release-API wird abgewiesen, bevor eine + // Verbindung aufgebaut wird + let agent = create_http_agent(); + let err = http_get_with_auth_guard( + &agent, + "http://127.0.0.1:9/ink-notes.AppImage", + Some("geheim"), + 5, + false, + ) + .unwrap_err(); + assert!(err.contains("HTTPS"), "{err}"); + } + #[test] fn test_appimage_atomic_self_update() { let temp_dir = tempfile::tempdir().unwrap();