mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
133 lines
4.3 KiB
Rust
133 lines
4.3 KiB
Rust
#![no_main]
|
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
mod path_containment_storage_api;
|
|
use path_containment_storage_api::{check_object_name_for_length_and_slash, has_bad_path_component, is_valid_object_prefix};
|
|
use rustfs_utils::path::{clean, path_join};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
const ROOT: &str = "/srv/rustfs";
|
|
|
|
fn parse_case(data: &[u8]) -> (String, String, String, Vec<String>) {
|
|
let text = String::from_utf8_lossy(data);
|
|
let mut parts = text.splitn(4, '\n');
|
|
let bucket = parts.next().unwrap_or_default().to_string();
|
|
let object = parts.next().unwrap_or_default().to_string();
|
|
let extra = parts.next().unwrap_or_default().to_string();
|
|
let flags = parts
|
|
.next()
|
|
.unwrap_or_default()
|
|
.split(',')
|
|
.map(str::trim)
|
|
.filter(|flag| !flag.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.collect();
|
|
|
|
(bucket, object, extra, flags)
|
|
}
|
|
|
|
fn stays_within_root(root: &Path, candidate: &Path) -> bool {
|
|
candidate == root || candidate.starts_with(root)
|
|
}
|
|
|
|
fn apply_flag(mut object: String, extra: &str, flag: &str) -> String {
|
|
let extra_segment = if extra.is_empty() { "tail" } else { extra };
|
|
|
|
match flag {
|
|
"leading_ws" => format!(" {object}"),
|
|
"trailing_ws" => {
|
|
object.push(' ');
|
|
object
|
|
}
|
|
"leading_slash" => format!("/{object}"),
|
|
"trailing_slash" => format!("{object}/"),
|
|
"double_sep" => {
|
|
if object.contains('/') {
|
|
object.replacen('/', "//", 1)
|
|
} else {
|
|
format!("{object}//{extra_segment}")
|
|
}
|
|
}
|
|
"backslash" => object.replace('/', "\\"),
|
|
"append_extra" => format!("{object}/{extra_segment}"),
|
|
"empty_segment" => {
|
|
if let Some((left, right)) = object.split_once('/') {
|
|
format!("{left}//{right}")
|
|
} else {
|
|
format!("{object}//{extra_segment}")
|
|
}
|
|
}
|
|
"parent_segment" => format!("{object}/../{extra_segment}"),
|
|
"current_segment" => format!("{object}/./{extra_segment}"),
|
|
_ => object,
|
|
}
|
|
}
|
|
|
|
fn materialize_object(base: String, extra: &str, flags: &[String]) -> String {
|
|
flags.iter().fold(base, |object, flag| apply_flag(object, extra, flag))
|
|
}
|
|
|
|
fn has_dot_segments(path: &str) -> bool {
|
|
path.split(['/', '\\']).any(|segment| {
|
|
let mut bytes = segment.as_bytes();
|
|
|
|
// Match ecstore's path-component check. `str::trim()` also trims
|
|
// Unicode whitespace such as vertical tab, which would reject object
|
|
// keys that the production validator accepts as ordinary path bytes.
|
|
while let Some((first, rest)) = bytes.split_first()
|
|
&& first.is_ascii_whitespace()
|
|
{
|
|
bytes = rest;
|
|
}
|
|
while let Some((last, rest)) = bytes.split_last()
|
|
&& last.is_ascii_whitespace()
|
|
{
|
|
bytes = rest;
|
|
}
|
|
|
|
bytes == b"." || bytes == b".."
|
|
})
|
|
}
|
|
|
|
fuzz_target!(|data: &[u8]| {
|
|
let (bucket, base_object, extra, flags) = parse_case(data);
|
|
let object = materialize_object(base_object, &extra, &flags);
|
|
|
|
let has_bad_component = has_bad_path_component(&object);
|
|
let is_valid_prefix = is_valid_object_prefix(&object);
|
|
let length_and_slash_ok = check_object_name_for_length_and_slash(&bucket, &object).is_ok();
|
|
|
|
if object.is_empty() || !(is_valid_prefix && length_and_slash_ok) {
|
|
return;
|
|
}
|
|
|
|
assert!(
|
|
!has_bad_component,
|
|
"accepted object unexpectedly retained a bad path component: {:?}",
|
|
object
|
|
);
|
|
assert!(
|
|
!has_dot_segments(&object),
|
|
"accepted object unexpectedly retained dot segments: {:?}",
|
|
object
|
|
);
|
|
|
|
let root = PathBuf::from(ROOT);
|
|
let joined = path_join(&[root.clone(), PathBuf::from(&object)]);
|
|
let cleaned = PathBuf::from(clean(joined.to_string_lossy().as_ref()));
|
|
let cleaned_text = cleaned.to_string_lossy();
|
|
|
|
assert!(
|
|
stays_within_root(&root, &cleaned),
|
|
"accepted object escaped root containment: object={:?} cleaned={:?}",
|
|
object,
|
|
cleaned
|
|
);
|
|
assert!(
|
|
!has_dot_segments(cleaned_text.as_ref()),
|
|
"cleaned accepted object unexpectedly retained dot segments: object={:?} cleaned={:?}",
|
|
object,
|
|
cleaned
|
|
);
|
|
});
|