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:
1 parent
598fb5c679
commit
159f9e3f38
12 files changed
+545
-153
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user