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
27 changes: 10 additions & 17 deletions crates/bashkit/src/fs/realfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,23 +369,16 @@ impl FsBackend for RealFs {
Ok(())
}

async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
self.check_writable()?;
let real_link = self.resolve(link)?;
// Store the target as-is (virtual path) - symlinks are stored but
// not followed per security policy (TM-ESC-002)
#[cfg(unix)]
tokio::fs::symlink(target, &real_link).await?;
#[cfg(not(unix))]
{
let _ = (target, &real_link);
return Err(IoError::new(
ErrorKind::Unsupported,
"symlinks not supported on this platform",
)
.into());
}
Ok(())
/// THREAT[TM-ESC-003]: Symlink creation is blocked in RealFs to prevent
/// sandbox escape. Even though bashkit itself doesn't follow symlinks
/// (TM-ESC-002), any external process sharing the directory tree would
/// follow them, enabling reads/writes to arbitrary host paths.
async fn symlink(&self, _target: &Path, _link: &Path) -> Result<()> {
Err(IoError::new(
ErrorKind::PermissionDenied,
"symlink creation is not allowed in RealFs (sandbox security)",
)
.into())
}

async fn read_link(&self, path: &Path) -> Result<PathBuf> {
Expand Down
45 changes: 45 additions & 0 deletions crates/bashkit/tests/realfs_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,48 @@ async fn direct_fs_api_exists() {
assert!(fs.exists(Path::new("/mnt/data/hello.txt")).await.unwrap());
assert!(!fs.exists(Path::new("/mnt/data/nope.txt")).await.unwrap());
}

// ==================== Symlink sandbox escape prevention (Issue #979) ====================

#[tokio::test]
async fn realfs_symlink_absolute_escape_blocked() {
let dir = setup_host_dir();
let mut bash = Bash::builder()
.mount_real_readwrite_at(dir.path(), "/mnt/workspace")
.build();

// Attempt to create a symlink pointing to /etc/passwd
let r = bash
.exec("ln -s /etc/passwd /mnt/workspace/escape 2>&1; echo $?")
.await
.unwrap();
// Should fail with non-zero exit code
assert!(
r.stdout.trim().ends_with('1')
|| r.stdout.contains("not allowed")
|| r.stdout.contains("Permission denied"),
"Symlink creation should be blocked, got: {}",
r.stdout
);
}

#[tokio::test]
async fn realfs_symlink_relative_escape_blocked() {
let dir = setup_host_dir();
let mut bash = Bash::builder()
.mount_real_readwrite_at(dir.path(), "/mnt/workspace")
.build();

// Attempt relative path traversal via symlink
let r = bash
.exec("ln -s ../../../../etc/passwd /mnt/workspace/escape 2>&1; echo $?")
.await
.unwrap();
assert!(
r.stdout.trim().ends_with('1')
|| r.stdout.contains("not allowed")
|| r.stdout.contains("Permission denied"),
"Relative symlink escape should be blocked, got: {}",
r.stdout
);
}
Loading