|
|
|
@@ -143,6 +143,10 @@ pub fn parse_vds_mappings(
|
|
|
|
|
let source_selection = read_selection(heap_data, &mut pos)?;
|
|
|
|
|
let virtual_selection = read_selection(heap_data, &mut pos)?;
|
|
|
|
|
|
|
|
|
|
// Validate external file name to prevent directory traversal attacks
|
|
|
|
|
// (Dataset paths within files can use absolute HDF5 paths like "/data")
|
|
|
|
|
validate_vds_file_name(&source_file)?;
|
|
|
|
|
|
|
|
|
|
mappings.push(VdsMapping {
|
|
|
|
|
source_file,
|
|
|
|
|
source_dataset,
|
|
|
|
@@ -154,6 +158,37 @@ pub fn parse_vds_mappings(
|
|
|
|
|
Ok(mappings)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate external file names to prevent directory traversal.
|
|
|
|
|
/// Dataset paths within files can use absolute HDF5 paths (starting with /),
|
|
|
|
|
/// but external file names must not escape the file tree via .. or absolute paths.
|
|
|
|
|
fn validate_vds_file_name(filename: &str) -> Result<(), FormatError> {
|
|
|
|
|
if filename.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// "." means same file - always OK
|
|
|
|
|
if filename == "." {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filesystem paths cannot start with / (absolute filesystem path)
|
|
|
|
|
if filename.starts_with('/') {
|
|
|
|
|
return Err(FormatError::FilterError(
|
|
|
|
|
"VDS file name cannot be an absolute filesystem path".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reject directory traversal (..)
|
|
|
|
|
if filename.contains("..") {
|
|
|
|
|
return Err(FormatError::FilterError(
|
|
|
|
|
"VDS file name contains illegal traversal sequence (..)".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Relative filesystem paths are OK
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a null-terminated UTF-8 string from data starting at `pos`.
|
|
|
|
|
fn read_null_terminated_string(data: &[u8], pos: &mut usize) -> Result<String, FormatError> {
|
|
|
|
|
let start = *pos;
|
|
|
|
@@ -862,4 +897,68 @@ mod tests {
|
|
|
|
|
let blob = [0x01u8, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
|
|
|
assert!(parse_vds_mappings(&blob, 8).unwrap().is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_vds_mappings_rejects_path_traversal() {
|
|
|
|
|
// INT-06: Verify that VDS file names containing ".." are rejected
|
|
|
|
|
let blob = [
|
|
|
|
|
0x00u8, // version 0 (with explicit file name)
|
|
|
|
|
0x01, 0, 0, 0, 0, 0, 0, 0, // nused = 1
|
|
|
|
|
0x2e, 0x2e, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x00, // "../etc/passwd |