fix(security): backport upstream modpack path traversal fixes

Backport Mystic-Stars/Axolotl b2f81391e584aebf3ef07e8db2fc9cdad2ffb4c7. Adapt validation to this fork and protect CurseForge archive paths and MRPack uninstall cleanup.
This commit is contained in:
Disy committed 2026-10-20 17:04:46 +08:00
1 parent 598fb5c679
commit 159f9e3f38
12 files changed
+545 -153

No files matched your search

+70 -18
View File
@@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::future::Future;
use std::path::{Component, Path, PathBuf};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use zip::ZipArchive;
@@ -320,23 +320,13 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
"Specified version has no files".to_string(),
)
})?;
let file_name = Path::new(&pack_file.filename);
if file_name.components().count() != 1
|| !matches!(file_name.components().next(), Some(Component::Normal(_)))
{
return Err(crate::ErrorKind::InputError(
"Modrinth returned an invalid modpack file name".to_string(),
)
.into());
}
let hash = pack_file.hashes.get("sha1");
let pack_path = state
.directories
.caches_dir()
.join("modpacks")
.join(&project_id)
.join(&version_id)
.join(file_name);
let pack_path = modpack_cache_path(
&state.directories.caches_dir().join("modpacks"),
&project_id,
&version_id,
&pack_file.filename,
)?;
let details = InstallPhaseDetails::Modpack {
project_id: Some(project_id.clone()),
@@ -517,8 +507,70 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
})
}
#[tracing::instrument]
fn modpack_cache_path(
root: &Path,
project_id: &str,
version_id: &str,
filename: &str,
) -> crate::Result<PathBuf> {
let mut path = root.to_path_buf();
for segment in [project_id, version_id, filename] {
if segment.contains('/') {
return Err(crate::ErrorKind::InputError(
"Invalid modpack cache path component".to_string(),
)
.into());
}
path = crate::util::io::safe_relative_join(&path, segment)?;
}
Ok(path)
}
#[cfg(test)]
mod cache_path_tests {
use super::modpack_cache_path;
use std::path::Path;
#[test]
fn cache_components_cannot_escape_their_directories() {
let root = Path::new("modpacks");
assert_eq!(
modpack_cache_path(root, "project", "version", "pack.mrpack")
.unwrap(),
root.join("project/version/pack.mrpack")
);
for malicious in [
"../outside",
"nested/name",
r"..\outside",
"C:/outside",
"C:outside",
r"\\server\share",
".",
"",
"NUL",
"pack.mrpack:stream",
".. ",
] {
for position in 0..3 {
let mut components = ["project", "version", "pack.mrpack"];
components[position] = malicious;
assert!(
modpack_cache_path(
root,
components[0],
components[1],
components[2]
)
.is_err(),
"{position}: {malicious:?}"
);
}
}
}
}
#[tracing::instrument]
pub async fn generate_pack_from_file(
path: PathBuf,
instance_id: String,