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
@@ -60,6 +60,34 @@ pub(crate) fn io_error_with_lock_info(
|
||||
io_error_with_lock_info_for_paths(source, path, &[path])
|
||||
}
|
||||
|
||||
/// Joins an untrusted Unix-style relative file path, rejecting traversal and
|
||||
/// Windows aliases on every platform. This is a lexical check, not a symlink
|
||||
/// sandbox.
|
||||
pub(crate) fn safe_relative_join(
|
||||
root: &Path,
|
||||
relative: &str,
|
||||
) -> crate::Result<PathBuf> {
|
||||
path_util::SafeRelativeUtf8UnixPathBuf::try_from(relative.to_string())
|
||||
.map_err(|error| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid file path {relative}: {error}"
|
||||
))
|
||||
})?;
|
||||
if relative.split('/').any(|segment| {
|
||||
segment.is_empty()
|
||||
|| segment == "."
|
||||
|| segment.ends_with(['.', ' '])
|
||||
|| segment.contains(':')
|
||||
|| segment.chars().any(char::is_control)
|
||||
}) {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Invalid file path: {relative}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
Ok(root.join(relative))
|
||||
}
|
||||
|
||||
/// Join a user-provided relative path onto `root`, rejecting traversal.
|
||||
pub(crate) fn join_within_root(
|
||||
root: &Path,
|
||||
@@ -95,7 +123,7 @@ pub(crate) fn join_within_root(
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(root.join(safe))
|
||||
safe_relative_join(root, &safe.to_string_lossy().replace('\\', "/"))
|
||||
}
|
||||
|
||||
pub(crate) fn io_error_with_lock_info_for_paths(
|
||||
@@ -895,6 +923,39 @@ mod windows_sharing_violation_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod path_join_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn join_within_root_uses_portable_path_validation() {
|
||||
let root = Path::new("root");
|
||||
assert_eq!(
|
||||
join_within_root(root, r"config\example.toml").unwrap(),
|
||||
root.join("config/example.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
join_within_root(root, "./config/./example.toml").unwrap(),
|
||||
root.join("config/example.toml")
|
||||
);
|
||||
for relative in [
|
||||
"../outside",
|
||||
r"..\outside",
|
||||
"/outside",
|
||||
"C:/outside",
|
||||
"C:outside",
|
||||
r"\\server\share",
|
||||
"config/.. /outside",
|
||||
"config/file:stream",
|
||||
"config/NUL",
|
||||
"config/file.",
|
||||
"config/file\0",
|
||||
] {
|
||||
assert!(join_within_root(root, relative).is_err(), "{relative:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod copy_dir_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user