Skip to content

Commit 4e2922e

Browse files
authored
fix(security): block symlink creation in RealFs to prevent sandbox escape (#1027)
## Summary - Block symlink creation in RealFs entirely to prevent sandbox escape - External processes sharing the directory tree would follow symlinks to arbitrary host paths ## Why `ln -s /etc/passwd link` inside a RealFs sandbox created an actual symlink on the host. While bashkit doesn't follow symlinks internally (TM-ESC-002), CI runners or other container processes sharing the directory would follow them. ## Tests - `realfs_symlink_absolute_escape_blocked` — `ln -s /etc/passwd` is rejected - `realfs_symlink_relative_escape_blocked` — `ln -s ../../../../etc/passwd` is rejected Closes #979
1 parent 2952caf commit 4e2922e

File tree

2 files changed

+55
-17
lines changed

2 files changed

+55
-17
lines changed

crates/bashkit/src/fs/realfs.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -369,23 +369,16 @@ impl FsBackend for RealFs {
369369
Ok(())
370370
}
371371

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

391384
async fn read_link(&self, path: &Path) -> Result<PathBuf> {

crates/bashkit/tests/realfs_tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,48 @@ async fn direct_fs_api_exists() {
297297
assert!(fs.exists(Path::new("/mnt/data/hello.txt")).await.unwrap());
298298
assert!(!fs.exists(Path::new("/mnt/data/nope.txt")).await.unwrap());
299299
}
300+
301+
// ==================== Symlink sandbox escape prevention (Issue #979) ====================
302+
303+
#[tokio::test]
304+
async fn realfs_symlink_absolute_escape_blocked() {
305+
let dir = setup_host_dir();
306+
let mut bash = Bash::builder()
307+
.mount_real_readwrite_at(dir.path(), "/mnt/workspace")
308+
.build();
309+
310+
// Attempt to create a symlink pointing to /etc/passwd
311+
let r = bash
312+
.exec("ln -s /etc/passwd /mnt/workspace/escape 2>&1; echo $?")
313+
.await
314+
.unwrap();
315+
// Should fail with non-zero exit code
316+
assert!(
317+
r.stdout.trim().ends_with('1')
318+
|| r.stdout.contains("not allowed")
319+
|| r.stdout.contains("Permission denied"),
320+
"Symlink creation should be blocked, got: {}",
321+
r.stdout
322+
);
323+
}
324+
325+
#[tokio::test]
326+
async fn realfs_symlink_relative_escape_blocked() {
327+
let dir = setup_host_dir();
328+
let mut bash = Bash::builder()
329+
.mount_real_readwrite_at(dir.path(), "/mnt/workspace")
330+
.build();
331+
332+
// Attempt relative path traversal via symlink
333+
let r = bash
334+
.exec("ln -s ../../../../etc/passwd /mnt/workspace/escape 2>&1; echo $?")
335+
.await
336+
.unwrap();
337+
assert!(
338+
r.stdout.trim().ends_with('1')
339+
|| r.stdout.contains("not allowed")
340+
|| r.stdout.contains("Permission denied"),
341+
"Relative symlink escape should be blocked, got: {}",
342+
r.stdout
343+
);
344+
}

0 commit comments

Comments
 (0)