Skip to content

Commit 2fd3302

Browse files
committed
Fix potential unsafety of joining path and building and parsing GAV
1 parent 59d97ef commit 2fd3302

6 files changed

Lines changed: 124 additions & 133 deletions

File tree

portablemc/src/base/mod.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -608,14 +608,22 @@ impl Installer {
608608
// If we find a arch replacement pattern, we must replace it with
609609
// the target architecture bit-ness (32, 64).
610610
const ARCH_REPLACEMENT_PATTERN: &str = "${arch}";
611+
let new_gav;
611612
if let Some(pattern_idx) = classifier.find(ARCH_REPLACEMENT_PATTERN) {
612613
let mut classifier = classifier.clone();
613614
classifier.replace_range(pattern_idx..pattern_idx + ARCH_REPLACEMENT_PATTERN.len(), os_bits);
614-
lib_gav.set_classifier(Some(&classifier));
615+
new_gav = lib_gav.with_classifier(Some(&classifier));
615616
} else {
616-
lib_gav.set_classifier(Some(&classifier));
617+
new_gav = lib_gav.with_classifier(Some(&classifier));
617618
}
618619

620+
// Ignore that new GAV if the gav cannot be constructed!
621+
let Some(new_gav) = new_gav else {
622+
continue;
623+
};
624+
625+
lib_gav = new_gav;
626+
619627
}
620628

621629
// Start by applying rules before the actual parsing. Important, we do
@@ -629,8 +637,9 @@ impl Installer {
629637

630638
// Clone the spec with wildcard for version because we shouldn't override
631639
// if any of the group/artifact/classifier/extension are matching.
632-
let mut lib_gav_wildcard = lib_gav.clone();
633-
lib_gav_wildcard.set_version("*");
640+
// Unwrapping because it's a single character version, which could not
641+
// exceed the limits checked by the previous version.
642+
let lib_gav_wildcard = lib_gav.with_version("*").unwrap();
634643
if !libraries_set.insert(lib_gav_wildcard) {
635644
continue;
636645
}
@@ -705,8 +714,7 @@ impl Installer {
705714

706715
// Construct the library path depending on its presence.
707716
let lib_file = if let Some(lib_rel_path) = lib.path.as_deref() {
708-
// FIXME: Insecure path joining.
709-
self.libraries_dir.join(lib_rel_path)
717+
self.libraries_dir.join(check_path_relative_and_safe(lib_rel_path)?)
710718
} else {
711719
lib.name.file(&self.libraries_dir)
712720
};
@@ -2270,6 +2278,16 @@ struct MojangJvmLink {
22702278
target_file: Box<Path>,
22712279
}
22722280

2281+
/// Check that the given path does not contain any root or parent directory component.
2282+
pub(crate) fn check_path_relative_and_safe<P: AsRef<Path> + ?Sized>(path: &P) -> Result<&Path> {
2283+
let path = path.as_ref();
2284+
if path.is_relative_and_safe() {
2285+
Ok(path)
2286+
} else {
2287+
Err(Error::new_io_file(io::Error::new(io::ErrorKind::InvalidInput, "the is not relative or contains unsafe components"), path))
2288+
}
2289+
}
2290+
22732291
/// Check if a file at a given path has the corresponding properties (size and/or SHA-1),
22742292
/// returning true if it is valid, so false is returned anyway if the file doesn't exists.
22752293
pub(crate) fn check_file(file: &Path, size: Option<u32>, sha1: Option<&[u8; 20]>) -> Result<bool> {

portablemc/src/base/serde.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use std::collections::HashMap;
77
use std::fmt;
8+
use std::path::PathBuf;
89

910
use chrono::{DateTime, FixedOffset};
1011

@@ -152,7 +153,7 @@ impl VersionLibraryDownloads {
152153
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
153154
#[serde(rename_all = "camelCase")]
154155
pub struct VersionLibraryDownload {
155-
pub path: Option<String>,
156+
pub path: Option<PathBuf>,
156157
#[serde(flatten)]
157158
pub download: Download,
158159
}

portablemc/src/forge/mod.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ impl Installer {
157157
let libs_dir = mojang.base().libraries_dir();
158158

159159
// Start by checking patched client and universal client.
160-
if !check_exists(&config.name.with_classifier(Some("client")).file(libs_dir)) {
160+
if let Some(client_gav) = config.name.with_classifier(Some("client")) && !check_exists(&client_gav.file(libs_dir)) {
161161
break InstallReason::MissingPatchedClient;
162162
}
163163

164-
if !check_exists(&config.name.with_classifier(Some("universal")).file(libs_dir)) {
164+
if let Some(universal_gav) = config.name.with_classifier(Some("universal")) && !check_exists(&universal_gav.file(libs_dir)) {
165165
break InstallReason::MissingUniversalClient;
166166
}
167167

@@ -704,7 +704,7 @@ impl InstallConfig {
704704

705705
Some(Self {
706706
default_prefix: "forge",
707-
name: Gav::new("net.minecraftforge", "forge", name, None, None),
707+
name: Gav::new("net.minecraftforge", "forge", name, None, None)?,
708708
repo_url: "https://maven.minecraftforge.net",
709709
game_version: if game_version == "1.7.10_pre4" {
710710
"1.7.10-pre4".to_string() // The only pre-release supported.
@@ -746,10 +746,10 @@ impl InstallConfig {
746746
let game_version;
747747

748748
if name == "47.1.82" || name.starts_with("1.20.1-") {
749-
gav = Gav::new("net.neoforged", "forge", name, None, None);
749+
gav = Gav::new("net.neoforged", "forge", name, None, None)?;
750750
game_version = "1.20.1".to_string();
751751
} else {
752-
gav = Gav::new("net.neoforged", "neoforge", name, None, None);
752+
gav = Gav::new("net.neoforged", "neoforge", name, None, None)?;
753753
game_version = match parse_generic_version::<2, 2>(name)? {
754754
[major, 0] => format!("1.{major}"),
755755
[major, minor] => format!("1.{major}.{minor}"),
@@ -788,7 +788,13 @@ fn try_install(
788788
// simply no installer for this version!
789789
handler.on_event(Event::FetchInstaller { version: config.name.version() });
790790

791-
let installer_gav = config.name.with_classifier(Some("installer"));
791+
// Handle the case where the next installer gav could not be constructed, too long?
792+
let Some(installer_gav) = config.name.with_classifier(Some("installer")) else {
793+
return Err(Error::InstallerNotFound {
794+
version: config.name.version().to_string(),
795+
});
796+
};
797+
792798
let installer_url = format!("{}/{}", config.repo_url, installer_gav.url());
793799

794800
// Download and check result in case installer is just not found.
@@ -904,8 +910,7 @@ fn try_install(
904910
let lib_dl = &lib.downloads.artifact;
905911

906912
let lib_file = if let Some(lib_path) = &lib_dl.path {
907-
// FIXME: Insecure joining!
908-
libraries_dir.join(lib_path)
913+
libraries_dir.join(base::check_path_relative_and_safe(lib_path)?)
909914
} else {
910915
lib.name.file(&libraries_dir)
911916
};
@@ -949,9 +954,8 @@ fn try_install(
949954
}
950955
_ => {
951956
// This is a file that we should extract to the temp directory.
952-
// FIXME: Insecure joining.
953957
let entry = entry.strip_prefix('/').unwrap_or(entry);
954-
let tmp_file = tmp_dir.join(entry);
958+
let tmp_file = tmp_dir.join(base::check_path_relative_and_safe(entry)?);
955959
extract_installer_file(installer_file, &mut installer_zip, entry, &tmp_file)?;
956960
InstallDataTypedEntry::File(tmp_file)
957961
}

0 commit comments

Comments
 (0)