Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions core/services/compfs/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ impl Access for CompfsBackend {
}

async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result<RpCreateDir> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;

self.core
.exec(move || async move { compio::fs::create_dir_all(path).await })
Expand All @@ -186,7 +186,7 @@ impl Access for CompfsBackend {
}

async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
let meta = self
.core
.exec(move || async move { compio::fs::metadata(path).await })
Expand Down Expand Up @@ -220,8 +220,8 @@ impl Access for CompfsBackend {
_: OpCopy,
_opts: OpCopier,
) -> Result<(RpCopy, Self::Copier)> {
let from = self.core.prepare_path(from);
let to = self.core.prepare_path(to);
let from = self.core.prepare_path(from)?;
let to = self.core.prepare_path(to)?;

self.core
.exec(move || async move {
Expand All @@ -247,8 +247,8 @@ impl Access for CompfsBackend {
}

async fn rename(&self, from: &str, to: &str, _: OpRename) -> Result<RpRename> {
let from = self.core.prepare_path(from);
let to = self.core.prepare_path(to);
let from = self.core.prepare_path(from)?;
let to = self.core.prepare_path(to)?;

self.core
.exec(move || async move {
Expand All @@ -262,7 +262,7 @@ impl Access for CompfsBackend {
Ok(RpRename::default())
}
async fn read(&self, path: &str, _: OpRead) -> Result<(RpRead, Self::Reader)> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
let file = self
.core
.exec(move || async move {
Expand All @@ -281,7 +281,7 @@ impl Access for CompfsBackend {
}

async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
let append = args.append();
let file = self
.core
Expand Down Expand Up @@ -309,7 +309,7 @@ impl Access for CompfsBackend {
}

async fn list(&self, path: &str, _: OpList) -> Result<(RpList, Self::Lister)> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;

let read_dir = match self
.core
Expand Down
65 changes: 63 additions & 2 deletions core/services/compfs/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// under the License.

use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

Expand Down Expand Up @@ -63,8 +64,23 @@ pub(super) struct CompfsCore {
}

impl CompfsCore {
pub fn prepare_path(&self, path: &str) -> PathBuf {
self.root.join(path.trim_end_matches('/'))
/// Reject `..` traversal in a key so it cannot escape `root`, matching the
/// `fs` backend (#7684). Confinement lives here because `normalize_path`
/// deliberately leaves `.`/`..` unresolved (RFC 0112).
pub fn prepare_path(&self, path: &str) -> Result<PathBuf> {
use std::path::Component;
let trimmed = path.trim_end_matches('/');
if Path::new(trimmed)
.components()
.any(|c| matches!(c, Component::ParentDir))
{
return Err(Error::new(
ErrorKind::NotFound,
"path escapes the configured root via `..`",
)
.with_context("path", path));
}
Ok(self.root.join(trimmed))
}

pub async fn exec<Fn, Fut, R>(&self, f: Fn) -> opendal_core::Result<R>
Expand Down Expand Up @@ -136,4 +152,49 @@ mod tests {
assert_eq!(buf.total_len(), len);
assert_eq!(collected, bytes);
}

fn new_test_core() -> CompfsCore {
CompfsCore {
info: Arc::new(AccessorInfo::default()),
root: PathBuf::from("/data/root"),
dispatcher: Dispatcher::new().unwrap(),
buf_pool: oio::PooledBuf::new(16),
}
}

#[test]
fn test_prepare_path_rejects_parent_dir() {
let core = new_test_core();
for key in ["../etc/passwd", "../../etc/passwd", "a/../../b", "a/.."] {
let err = core.prepare_path(key).unwrap_err();
assert_eq!(
err.kind(),
ErrorKind::NotFound,
"key should be rejected: {key}"
);
}
}

#[test]
fn test_prepare_path_allows_normal_keys() {
let core = new_test_core();
// Normal keys, `.` (CurDir), and trailing slashes resolve unchanged.
assert_eq!(
core.prepare_path("a/b.txt").unwrap(),
PathBuf::from("/data/root/a/b.txt")
);
assert_eq!(
core.prepare_path("a/b/").unwrap(),
PathBuf::from("/data/root/a/b")
);
assert_eq!(
core.prepare_path("./a/b").unwrap(),
PathBuf::from("/data/root/a/b")
);
// A key containing `..` only as a substring of a name is not a traversal.
assert_eq!(
core.prepare_path("a..b").unwrap(),
PathBuf::from("/data/root/a..b")
);
}
}
4 changes: 2 additions & 2 deletions core/services/compfs/src/deleter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ impl CompfsDeleter {
impl oio::OneShotDelete for CompfsDeleter {
async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> {
let res = if path.ends_with('/') {
let path = self.core.prepare_path(&path);
let path = self.core.prepare_path(&path)?;
self.core
.exec(move || async move { compio::fs::remove_dir(path).await })
.await
} else {
let path = self.core.prepare_path(&path);
let path = self.core.prepare_path(&path)?;
self.core
.exec(move || async move { compio::fs::remove_file(path).await })
.await
Expand Down
10 changes: 5 additions & 5 deletions core/services/monoiofs/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Access for MonoiofsBackend {
}

async fn stat(&self, path: &str, _args: OpStat) -> Result<RpStat> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
let meta = self
.core
.dispatch(move || monoio::fs::metadata(path))
Expand All @@ -125,7 +125,7 @@ impl Access for MonoiofsBackend {
Ok(RpStat::new(m))
}
async fn read(&self, path: &str, _args: OpRead) -> Result<(RpRead, Self::Reader)> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
let reader = MonoiofsReader::new(self.core.clone(), path).await?;
Ok((RpRead::default(), oio::PositionReader::new(reader)))
}
Expand All @@ -144,7 +144,7 @@ impl Access for MonoiofsBackend {
}

async fn rename(&self, from: &str, to: &str, _args: OpRename) -> Result<RpRename> {
let from = self.core.prepare_path(from);
let from = self.core.prepare_path(from)?;
// ensure file exists
self.core
.dispatch({
Expand All @@ -162,7 +162,7 @@ impl Access for MonoiofsBackend {
}

async fn create_dir(&self, path: &str, _args: OpCreateDir) -> Result<RpCreateDir> {
let path = self.core.prepare_path(path);
let path = self.core.prepare_path(path)?;
self.core
.dispatch(move || monoio::fs::create_dir_all(path))
.await
Expand All @@ -177,7 +177,7 @@ impl Access for MonoiofsBackend {
_args: OpCopy,
_opts: OpCopier,
) -> Result<(RpCopy, Self::Copier)> {
let from = self.core.prepare_path(from);
let from = self.core.prepare_path(from)?;
// ensure file exists
self.core
.dispatch({
Expand Down
59 changes: 55 additions & 4 deletions core/services/monoiofs/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// under the License.

use std::mem;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
Expand Down Expand Up @@ -95,14 +96,28 @@ impl MonoiofsCore {
}
}

/// join root and path
pub fn prepare_path(&self, path: &str) -> PathBuf {
self.root.join(path.trim_end_matches('/'))
/// Reject `..` traversal in a key so it cannot escape `root`, matching the
/// `fs` backend (#7684). Confinement lives here because `normalize_path`
/// deliberately leaves `.`/`..` unresolved (RFC 0112).
pub fn prepare_path(&self, path: &str) -> Result<PathBuf> {
use std::path::Component;
let trimmed = path.trim_end_matches('/');
if Path::new(trimmed)
.components()
.any(|c| matches!(c, Component::ParentDir))
{
return Err(Error::new(
ErrorKind::NotFound,
"path escapes the configured root via `..`",
)
.with_context("path", path));
}
Ok(self.root.join(trimmed))
}

/// join root and path, create parent dirs
pub async fn prepare_write_path(&self, path: &str) -> Result<PathBuf> {
let path = self.prepare_path(path);
let path = self.prepare_path(path)?;
let parent = path
.parent()
.ok_or_else(|| {
Expand Down Expand Up @@ -309,4 +324,40 @@ mod tests {
let core = new_core(4);
dispatch_panic(core).await;
}

#[tokio::test]
async fn test_prepare_path_rejects_parent_dir() {
let core = Arc::new(MonoiofsCore::new(PathBuf::from("/data/root"), 1, 1024));
for key in ["../etc/passwd", "../../etc/passwd", "a/../../b", "a/.."] {
let err = core.prepare_path(key).unwrap_err();
assert_eq!(
err.kind(),
ErrorKind::NotFound,
"key should be rejected: {key}"
);
}
}

#[tokio::test]
async fn test_prepare_path_allows_normal_keys() {
let core = Arc::new(MonoiofsCore::new(PathBuf::from("/data/root"), 1, 1024));
// Normal keys, `.` (CurDir), and trailing slashes resolve unchanged.
assert_eq!(
core.prepare_path("a/b.txt").unwrap(),
PathBuf::from("/data/root/a/b.txt")
);
assert_eq!(
core.prepare_path("a/b/").unwrap(),
PathBuf::from("/data/root/a/b")
);
assert_eq!(
core.prepare_path("./a/b").unwrap(),
PathBuf::from("/data/root/a/b")
);
// A key containing `..` only as a substring of a name is not a traversal.
assert_eq!(
core.prepare_path("a..b").unwrap(),
PathBuf::from("/data/root/a..b")
);
}
}
2 changes: 1 addition & 1 deletion core/services/monoiofs/src/deleter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl MonoiofsDeleter {

impl oio::OneShotDelete for MonoiofsDeleter {
async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> {
let path = self.core.prepare_path(&path);
let path = self.core.prepare_path(&path)?;
let meta = self
.core
.dispatch({
Expand Down
Loading