diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index 5abf4173..71000650 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -9530,6 +9530,124 @@ printf 'pwsh:%s' "$1" registry } + + #[test] + fn path_scope_classifies_direct_paths_inside_and_outside_workspace() { + let _guard = env_guard(); + let root = temp_path("perm-path-direct"); + fs::create_dir_all(root.join("src")).expect("create workspace"); + fs::write(root.join("src/lib.rs"), "// ok\n").expect("write in workspace"); + let outside = temp_path("perm-path-direct-outside.txt"); + fs::write(&outside, "secret\n").expect("write outside file"); + let previous = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&root).expect("enter workspace"); + + assert_eq!( + super::classify_bash_permission("cat src/lib.rs"), + PermissionMode::WorkspaceWrite + ); + assert_eq!( + super::classify_bash_permission(&format!("cat {}", outside.display())), + PermissionMode::DangerFullAccess + ); + + std::env::set_current_dir(previous).expect("restore cwd"); + let _ = fs::remove_dir_all(root); + let _ = fs::remove_file(outside); + } + + #[cfg(unix)] + #[test] + fn path_scope_denies_symlink_that_resolves_outside_workspace() { + let _guard = env_guard(); + let root = temp_path("perm-path-symlink"); + fs::create_dir_all(&root).expect("create workspace"); + let outside = temp_path("perm-path-symlink-secret.txt"); + fs::write(&outside, "secret\n").expect("write outside file"); + std::os::unix::fs::symlink(&outside, root.join("secret-link")).expect("create symlink"); + let previous = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&root).expect("enter workspace"); + + assert_eq!( + super::classify_bash_permission("cat secret-link"), + PermissionMode::DangerFullAccess, + "workspace-relative symlinks resolving outside the workspace must not be downgraded to workspace-write" + ); + + std::env::set_current_dir(previous).expect("restore cwd"); + let _ = fs::remove_dir_all(root); + let _ = fs::remove_file(outside); + } + + #[test] + fn path_scope_denies_shell_expansion_and_glob_traversal() { + let _guard = env_guard(); + let root = temp_path("perm-path-expansion"); + fs::create_dir_all(&root).expect("create workspace"); + let previous = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&root).expect("enter workspace"); + + for command in [ + "cat ../*", + "cat $PWD/../secret.txt", + "cat ${PWD}/../secret.txt", + "cat $(pwd)/../secret.txt", + ] { + assert_eq!( + super::classify_bash_permission(command), + PermissionMode::DangerFullAccess, + "{command} should not be treated as workspace-scoped" + ); + } + + std::env::set_current_dir(previous).expect("restore cwd"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn path_scope_allows_nested_worktree_paths_but_denies_parent_escape() { + let _guard = env_guard(); + let root = temp_path("perm-path-worktree"); + let worktree = root.join("main").join("linked-worktree"); + fs::create_dir_all(worktree.join("src")).expect("create worktree"); + fs::write(worktree.join("src/lib.rs"), "// ok\n").expect("write worktree file"); + let previous = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&worktree).expect("enter worktree"); + + assert_eq!( + super::classify_bash_permission("cat src/lib.rs"), + PermissionMode::WorkspaceWrite + ); + assert_eq!( + super::classify_bash_permission("cat ../../outside.txt"), + PermissionMode::DangerFullAccess + ); + + std::env::set_current_dir(previous).expect("restore cwd"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn path_scope_denies_windows_style_absolute_paths() { + for command in [ + r"cat C:\\Users\\attacker\\secret.txt", + r"cat C:/Users/attacker/secret.txt", + r"Get-Content -Path C:\\Users\\attacker\\secret.txt", + r"Get-Content -Path C:/Users/attacker/secret.txt", + ] { + let mode = if command.starts_with("Get-Content") { + super::classify_powershell_permission(command) + } else { + super::classify_bash_permission(command) + }; + assert_eq!( + mode, + PermissionMode::DangerFullAccess, + "{command} should not be treated as workspace-scoped" + ); + } + } + #[test] fn given_read_only_enforcer_when_bash_then_denied() { let registry = read_only_registry();