From 31e9573fbf19a95ae9deb65c920936f7c2077a2f Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Sun, 6 Sep 2026 00:37:19 +0300 Subject: [PATCH] feat: restrict manifest downloads to ShaCraft hosts --- docs/manifest-v1.md | 5 +++-- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/manifest.rs | 18 ++++++++++++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/manifest-v1.md b/docs/manifest-v1.md index 3ebc2a7..95a1bc6 100644 --- a/docs/manifest-v1.md +++ b/docs/manifest-v1.md @@ -26,5 +26,6 @@ `managed` — файл контролирует лаунчер: при несовпадении SHA-256 он заменяется. `seed` — файл создаётся только при первом запуске и затем сохраняет изменения -игрока. В manifest v1 допускаются только HTTPS-адреса и относительные пути без -`..`, обратных слешей и пустых сегментов. +игрока. В manifest v1 допускаются только HTTPS-адреса на `shacraft.ru` и +`cdn.shacraft.ru`, а также относительные пути без `..`, обратных слешей и +пустых сегментов. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 787b26e..6a48185 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2802,6 +2802,7 @@ dependencies = [ "sha2", "tauri", "tauri-build", + "url", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fc167c0..d1e9082 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,3 +17,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" tauri = { version = "2", features = [] } +url = "2" diff --git a/src-tauri/src/manifest.rs b/src-tauri/src/manifest.rs index c96558b..1b65391 100644 --- a/src-tauri/src/manifest.rs +++ b/src-tauri/src/manifest.rs @@ -1,8 +1,10 @@ use serde::Deserialize; use std::{collections::HashSet, fmt}; +use url::Url; const MAX_MANIFEST_BYTES: usize = 2 * 1024 * 1024; const CURRENT_SCHEMA_VERSION: u32 = 1; +const DOWNLOAD_HOSTS: [&str; 2] = ["shacraft.ru", "cdn.shacraft.ru"]; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -106,8 +108,8 @@ fn validate(manifest: &Manifest) -> Result<(), ManifestError> { if !paths.insert(&file.path) { return Err(ManifestError::Invalid(format!("Duplicate file path: {}", file.path))); } - if !file.url.starts_with("https://") { - return Err(ManifestError::Invalid(format!("File URL must use HTTPS: {}", file.path))); + if !is_allowed_download_url(&file.url) { + return Err(ManifestError::Invalid(format!("File URL must use HTTPS and a ShaCraft host: {}", file.path))); } if file.size == 0 { return Err(ManifestError::Invalid(format!("File has zero size: {}", file.path))); @@ -133,6 +135,13 @@ fn is_safe_relative_path(value: &str) -> bool { && !value.split('/').any(|part| part.is_empty() || part == "." || part == "..") } +fn is_allowed_download_url(value: &str) -> bool { + let Ok(url) = Url::parse(value) else { + return false; + }; + url.scheme() == "https" && url.host_str().is_some_and(|host| DOWNLOAD_HOSTS.contains(&host)) +} + #[cfg(test)] mod tests { use super::validate_json; @@ -165,4 +174,9 @@ mod tests { fn rejects_insecure_downloads() { assert!(validate_json(&VALID.replace("https://", "http://")).is_err()); } + + #[test] + fn rejects_third_party_download_hosts() { + assert!(validate_json(&VALID.replace("cdn.shacraft.ru", "example.com")).is_err()); + } }