feat: restrict manifest downloads to ShaCraft hosts

This commit is contained in:
emil28092005
2026-09-06 00:37:19 +03:00
parent ace375b35c
commit 31e9573fbf
4 changed files with 21 additions and 4 deletions
+3 -2
View File
@@ -26,5 +26,6 @@
`managed` — файл контролирует лаунчер: при несовпадении SHA-256 он заменяется.
`seed` — файл создаётся только при первом запуске и затем сохраняет изменения
игрока. В manifest v1 допускаются только HTTPS-адреса и относительные пути без
`..`, обратных слешей и пустых сегментов.
игрока. В manifest v1 допускаются только HTTPS-адреса на `shacraft.ru` и
`cdn.shacraft.ru`, а также относительные пути без `..`, обратных слешей и
пустых сегментов.
+1
View File
@@ -2802,6 +2802,7 @@ dependencies = [
"sha2",
"tauri",
"tauri-build",
"url",
]
[[package]]
+1
View File
@@ -17,3 +17,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tauri = { version = "2", features = [] }
url = "2"
+16 -2
View File
@@ -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());
}
}