mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
upgrade-datapack: refuse pack entries that leave the pack
A zip entry named `../x` or `/x` (or `C:\x` on Windows) was written as it was, outside `<world>/datapacks/<name>-26.3/`. `read_pack` now takes each entry's name from `enclosed_name()` and refuses a pack with one that has anything but plain components, and `upgrade_into` refuses to write such a path. Review on #3369. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
81b3803017
commit
0771f59bca
1 file changed
+47
-1
@@ -231,9 +231,14 @@ pub fn read_pack(path: &Path) -> Result<BTreeMap<String, Vec<u8>>, String> {
|
||||
if entry.is_dir() {
|
||||
continue;
|
||||
}
|
||||
// A pack from the internet may name entries `../x` or `/x`.
|
||||
let name = entry
|
||||
.enclosed_name()
|
||||
.filter(|name| is_inside(name))
|
||||
.ok_or_else(|| at(&format!("entry {:?} leaves the pack", entry.name())))?;
|
||||
let mut bytes = Vec::new();
|
||||
entry.read_to_end(&mut bytes).map_err(|e| at(&e))?;
|
||||
files.insert(entry.name().replace('\\', "/"), bytes);
|
||||
files.insert(name.to_string_lossy().replace('\\', "/"), bytes);
|
||||
}
|
||||
}
|
||||
if !files.contains_key("pack.mcmeta") {
|
||||
@@ -263,6 +268,9 @@ pub fn upgrade_into(input: &Path, out: &Path) -> Result<Report, String> {
|
||||
let files = read_pack(input)?;
|
||||
let (upgraded, report) = upgrade_pack(&files);
|
||||
for (path, bytes) in upgraded {
|
||||
if !is_inside(Path::new(&path)) {
|
||||
return Err(format!("{path}: leaves {}", out.display()));
|
||||
}
|
||||
let target = out.join(&path);
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
|
||||
@@ -272,6 +280,14 @@ pub fn upgrade_into(input: &Path, out: &Path) -> Result<Report, String> {
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Whether `path` is a plain relative path: no root, drive, `.` or `..`.
|
||||
fn is_inside(path: &Path) -> bool {
|
||||
path.components().next().is_some()
|
||||
&& path
|
||||
.components()
|
||||
.all(|c| matches!(c, std::path::Component::Normal(_)))
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// The report as lines for the terminal.
|
||||
pub fn lines(&self) -> Vec<String> {
|
||||
@@ -357,6 +373,36 @@ mod tests {
|
||||
assert!(v.get("spawners").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entries_outside_the_pack_are_refused() {
|
||||
use std::io::Write;
|
||||
let dir = std::env::temp_dir().join(format!("upgrade-escape-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
for (i, name) in ["../escape.txt", "data/../../escape.txt", "/escape.txt"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let zip_path = dir.join(format!("p{i}.zip"));
|
||||
let mut zip = zip::ZipWriter::new(fs::File::create(&zip_path).unwrap());
|
||||
for (file, bytes) in [("pack.mcmeta", &b"{}"[..]), (name, b"x")] {
|
||||
zip.start_file(file, zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
}
|
||||
zip.finish().unwrap();
|
||||
let out = dir.join(format!("out{i}/datapacks/p-26.3"));
|
||||
let err = upgrade_into(&zip_path, &out).unwrap_err();
|
||||
assert!(err.contains("leaves the pack"), "{name}: {err}");
|
||||
assert!(!out.exists(), "{name}");
|
||||
}
|
||||
assert!(!dir.join("escape.txt").exists());
|
||||
assert!(is_inside(Path::new("data/x/worldgen/biome/b.json")));
|
||||
assert!(!is_inside(Path::new("data/../x.json")));
|
||||
assert!(!is_inside(Path::new("")));
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carvers_move_and_spell_out_their_hard_coded_shape() {
|
||||
let files = BTreeMap::from([(
|
||||
|
||||
Reference in new issue
Block a user