3bb1272c6d
validate_path_within_cwd used exists() to decide whether a path was new. exists() follows symlinks, so a *dangling* symlink (target missing) reported false and fell into the not-yet-existing branch, which only bound-checked the nearest existing ancestor — never the symlink itself. An approved `ln -s /etc/cron.d/pwn evil` inside the sandbox would therefore pass validation, and a subsequent Write to `evil` would follow the link and create a file outside the sandbox. In AcceptEdits mode this chained cleanly: approve one innocuous-looking symlink (Execute), then writes auto-allow. Resolve every path component with lstat (symlink_metadata, which does not follow links) instead of exists(): - Final component: if it is a symlink (dangling or live), resolve its real target via read_link + canonicalize-nearest-ancestor and reject unless the target stays within the sandbox. - Ancestor walk: an intermediate dangling symlink directory is likewise resolved and bound-checked rather than skipped. Live symlinks were already caught by canonicalize; this closes the dangling case. It does not close the check-vs-open TOCTOU (the open happens inside the SDK tool and would need O_NOFOLLOW/openat2 there); tracked for phase 2 along with OS-level bash isolation. Covered by tests/agent_sandbox.rs (dangling symlink to absolute and relative targets, write tool through a dangling link, write through an intermediate dangling symlink dir, all rejected; dangling link pointing inside and ordinary new files still allowed) plus mirrored in-crate unit tests.
66 lines
2.3 KiB
Rust
66 lines
2.3 KiB
Rust
//! Integration test for the dangling-symlink write-escape fix.
|
|
//!
|
|
//! Runs against the library's public API so it compiles/executes independently
|
|
//! of unrelated in-crate test fixtures. Reproduces the concrete escape an
|
|
//! approved `ln -s` could otherwise open: a symlink whose target does not exist
|
|
//! yet (so `exists()` is false and `canonicalize()` fails) must still be
|
|
//! rejected when it points outside the per-session sandbox, instead of being
|
|
//! treated as a brand-new in-sandbox path that a Write would follow out.
|
|
|
|
#![cfg(unix)]
|
|
|
|
use boosapi::services::agent_workbench::sandbox::{check_tool_paths, validate_path_within_cwd};
|
|
use serde_json::json;
|
|
use std::os::unix::fs::symlink;
|
|
use std::path::PathBuf;
|
|
|
|
fn tmp_sandbox() -> PathBuf {
|
|
let base = std::env::temp_dir().join(format!("agent_it_sb_{}", uuid::Uuid::new_v4().simple()));
|
|
std::fs::create_dir_all(&base).unwrap();
|
|
base.canonicalize().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn dangling_symlink_to_absolute_target_is_rejected() {
|
|
let sb = tmp_sandbox();
|
|
symlink("/etc/cron.d/pwn_it", sb.join("evil")).unwrap();
|
|
let res = validate_path_within_cwd("evil", &sb);
|
|
assert!(res.is_err(), "expected rejection, got {res:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn dangling_relative_symlink_escape_is_rejected() {
|
|
let sb = tmp_sandbox();
|
|
symlink("../../escape_it", sb.join("evil")).unwrap();
|
|
assert!(validate_path_within_cwd("evil", &sb).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn write_tool_through_dangling_symlink_is_blocked() {
|
|
let sb = tmp_sandbox();
|
|
symlink("/etc/cron.d/pwn_it2", sb.join("evil")).unwrap();
|
|
let input = json!({ "path": "evil", "content": "* * * * * root sh -c id" });
|
|
assert!(check_tool_paths("write", &input, &sb).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn write_through_intermediate_dangling_symlink_dir_is_blocked() {
|
|
let sb = tmp_sandbox();
|
|
symlink("/etc/nonexistent_it_dir", sb.join("evildir")).unwrap();
|
|
let input = json!({ "path": "evildir/pwn", "content": "x" });
|
|
assert!(check_tool_paths("write", &input, &sb).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn dangling_symlink_pointing_inside_sandbox_is_allowed() {
|
|
let sb = tmp_sandbox();
|
|
symlink(sb.join("future_file"), sb.join("ok_link")).unwrap();
|
|
assert!(validate_path_within_cwd("ok_link", &sb).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_new_file_still_allowed() {
|
|
let sb = tmp_sandbox();
|
|
assert!(validate_path_within_cwd("notes/today.md", &sb).is_ok());
|
|
}
|