mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 16:59:52 +00:00
fix(ecstore): reject symlink path escapes (#3742)
Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
@@ -150,11 +150,11 @@ pub fn access_std(path: impl AsRef<Path>) -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn lstat(path: impl AsRef<Path>) -> io::Result<Metadata> {
|
pub async fn lstat(path: impl AsRef<Path>) -> io::Result<Metadata> {
|
||||||
fs::metadata(path).await
|
fs::symlink_metadata(path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lstat_std(path: impl AsRef<Path>) -> io::Result<Metadata> {
|
pub fn lstat_std(path: impl AsRef<Path>) -> io::Result<Metadata> {
|
||||||
std::fs::metadata(path)
|
std::fs::symlink_metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn make_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
pub async fn make_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
||||||
@@ -355,6 +355,22 @@ mod tests {
|
|||||||
assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
|
assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_lstat_preserves_symlink_metadata() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let target_path = temp_dir.path().join("target.txt");
|
||||||
|
let link_path = temp_dir.path().join("link.txt");
|
||||||
|
|
||||||
|
tokio::fs::write(&target_path, b"test content").await.unwrap();
|
||||||
|
symlink(&target_path, &link_path).unwrap();
|
||||||
|
|
||||||
|
let metadata = lstat(&link_path).await.unwrap();
|
||||||
|
assert!(metadata.file_type().is_symlink());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lstat_std() {
|
fn test_lstat_std() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
@@ -369,6 +385,22 @@ mod tests {
|
|||||||
assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
|
assert_eq!(metadata.len(), 12); // "test content" is 12 bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_lstat_std_preserves_symlink_metadata() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let target_path = temp_dir.path().join("target-std.txt");
|
||||||
|
let link_path = temp_dir.path().join("link-std.txt");
|
||||||
|
|
||||||
|
std::fs::write(&target_path, b"test content").unwrap();
|
||||||
|
symlink(&target_path, &link_path).unwrap();
|
||||||
|
|
||||||
|
let metadata = lstat_std(&link_path).unwrap();
|
||||||
|
assert!(metadata.file_type().is_symlink());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_make_dir_all() {
|
async fn test_make_dir_all() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -991,11 +991,32 @@ impl LocalDisk {
|
|||||||
// Check if a path is valid
|
// Check if a path is valid
|
||||||
fn check_valid_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
fn check_valid_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||||
let path = normalize_path_components(path);
|
let path = normalize_path_components(path);
|
||||||
if path.starts_with(&self.root) {
|
if !path.starts_with(&self.root) {
|
||||||
Ok(())
|
return Err(DiskError::InvalidPath);
|
||||||
} else {
|
|
||||||
Err(DiskError::InvalidPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.reject_symlink_components(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_symlink_components(&self, path: &Path) -> Result<()> {
|
||||||
|
let relative = path.strip_prefix(&self.root).map_err(|_| DiskError::InvalidPath)?;
|
||||||
|
let mut current = self.root.clone();
|
||||||
|
|
||||||
|
for component in relative.components() {
|
||||||
|
current.push(component.as_os_str());
|
||||||
|
|
||||||
|
match lstat_std(¤t) {
|
||||||
|
Ok(metadata) => {
|
||||||
|
if metadata.file_type().is_symlink() {
|
||||||
|
return Err(DiskError::InvalidPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => break,
|
||||||
|
Err(err) => return Err(to_file_error(err).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch path generation with single lock acquisition
|
// Batch path generation with single lock acquisition
|
||||||
@@ -4456,6 +4477,42 @@ mod test {
|
|||||||
let _ = fs::remove_dir_all(&test_dir).await;
|
let _ = fs::remove_dir_all(&test_dir).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_bucket_path_rejects_symlink_escape() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let root_dir = tempdir().unwrap();
|
||||||
|
let outside_dir = tempdir().unwrap();
|
||||||
|
let link_path = root_dir.path().join("escape-bucket");
|
||||||
|
symlink(outside_dir.path(), &link_path).unwrap();
|
||||||
|
|
||||||
|
let endpoint = Endpoint::try_from(root_dir.path().to_string_lossy().as_ref()).unwrap();
|
||||||
|
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(disk.get_bucket_path("escape-bucket"), Err(DiskError::InvalidPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_object_path_rejects_symlink_component_escape() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let root_dir = tempdir().unwrap();
|
||||||
|
let outside_dir = tempdir().unwrap();
|
||||||
|
let bucket_dir = root_dir.path().join("bucket");
|
||||||
|
fs::create_dir_all(&bucket_dir).await.unwrap();
|
||||||
|
let link_path = bucket_dir.join("escape");
|
||||||
|
symlink(outside_dir.path(), &link_path).unwrap();
|
||||||
|
|
||||||
|
let endpoint = Endpoint::try_from(root_dir.path().to_string_lossy().as_ref()).unwrap();
|
||||||
|
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(disk.get_object_path("bucket", "escape/object.txt"), Err(DiskError::InvalidPath)));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_local_disk_file_operations() {
|
async fn test_local_disk_file_operations() {
|
||||||
let test_dir = "./test_local_disk_file_ops";
|
let test_dir = "./test_local_disk_file_ops";
|
||||||
|
|||||||
Reference in New Issue
Block a user