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
8 changes: 8 additions & 0 deletions crates/bashkit/src/fs/mountable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ impl MountableFs {
#[async_trait]
impl FileSystem for MountableFs {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
// THREAT[TM-DOS-046]: validate before delegation so mounted backends
// never receive control-character / depth-limit / length-limit
// violating paths through any read API.
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.read_file(&resolved).await
}
Expand Down Expand Up @@ -381,11 +385,13 @@ impl FileSystem for MountableFs {
}

async fn stat(&self, path: &Path) -> Result<Metadata> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.stat(&resolved).await
}

async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
self.validate_path(path)?;
let path = Self::normalize_path(path);
let (fs, resolved) = self.resolve(&path);

Expand Down Expand Up @@ -418,6 +424,7 @@ impl FileSystem for MountableFs {
}

async fn exists(&self, path: &Path) -> Result<bool> {
self.validate_path(path)?;
let path = Self::normalize_path(path);

// Check if this is a mount point
Expand Down Expand Up @@ -487,6 +494,7 @@ impl FileSystem for MountableFs {
}

async fn read_link(&self, path: &Path) -> Result<PathBuf> {
self.validate_path(path)?;
let (fs, resolved) = self.resolve(path);
fs.read_link(&resolved).await
}
Expand Down
79 changes: 79 additions & 0 deletions crates/bashkit/tests/security_audit_pocs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,85 @@ mod mountable_fs_validate_path {
let result = mountable.symlink(Path::new("/target"), bad_link).await;
assert!(result.is_err(), "MountableFs must validate symlink paths");
}

/// TM-DOS-046: MountableFs must validate paths on read_file.
#[tokio::test]
async fn security_audit_mountable_validates_read_file_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let bad_path = Path::new("/tmp/file\x01name");
let result = mountable.read_file(bad_path).await;
assert!(result.is_err(), "read_file must reject control characters");
}

/// TM-DOS-046: MountableFs must validate paths on stat.
#[tokio::test]
async fn security_audit_mountable_validates_stat_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let bad_path = Path::new("/tmp/file\x01name");
let result = mountable.stat(bad_path).await;
assert!(result.is_err(), "stat must reject control characters");
}

/// TM-DOS-046: MountableFs must validate paths on read_dir.
#[tokio::test]
async fn security_audit_mountable_validates_read_dir_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let bad_path = Path::new("/tmp/dir\x01name");
let result = mountable.read_dir(bad_path).await;
assert!(result.is_err(), "read_dir must reject control characters");
}

/// TM-DOS-046: MountableFs must validate paths on exists.
#[tokio::test]
async fn security_audit_mountable_validates_exists_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let bad_path = Path::new("/tmp/file\x01name");
let result = mountable.exists(bad_path).await;
assert!(result.is_err(), "exists must reject control characters");
}

/// TM-DOS-046: MountableFs must validate paths on read_link.
#[tokio::test]
async fn security_audit_mountable_validates_read_link_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let bad_path = Path::new("/tmp/link\x01name");
let result = mountable.read_link(bad_path).await;
assert!(result.is_err(), "read_link must reject control characters");
}

/// TM-DOS-046: MountableFs cross-mount rename must validate both ends.
#[tokio::test]
async fn security_audit_mountable_validates_rename_dest_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let src = Path::new("/tmp/src");
let bad_dest = Path::new("/tmp/dest\x01name");
let result = mountable.rename(src, bad_dest).await;
assert!(result.is_err(), "rename must reject control chars in dest");
}

/// TM-DOS-046: MountableFs cross-mount copy must validate both ends.
#[tokio::test]
async fn security_audit_mountable_validates_copy_dest_path() {
let root = Arc::new(InMemoryFs::new());
let mountable = MountableFs::new(root);

let src = Path::new("/tmp/src");
let bad_dest = Path::new("/tmp/dest\x01name");
let result = mountable.copy(src, bad_dest).await;
assert!(result.is_err(), "copy must reject control chars in dest");
}
}

// =============================================================================
Expand Down
Loading