use anyhow::{bail, Result};
static SHELL_INJECTION: &[&str] = &["$(", "`", "&&", "||", ";", "|", ">", "<", "\n", "\0"];
pub fn sanitize_tool_input(raw: &str) -> Result<String> {
for pat in SHELL_INJECTION {
if raw.contains(pat) {
bail!("input contains disallowed pattern: {:?}", pat);
}
}
Ok(raw.to_string())
}
pub fn jail_path(path: &str, root: &str) -> Result<String> {
let norm_path = normalize(std::path::Path::new(path));
let norm_root = normalize(std::path::Path::new(root));
if norm_path.starts_with(&norm_root) {
Ok(norm_path.to_string_lossy().into_owned())
} else {
bail!("path escapes jail: {path}")
}
}
fn normalize(path: &std::path::Path) -> std::path::PathBuf {
let mut out = std::path::PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::CurDir => {}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod unit {
use super::*;
#[test]
fn sanitize_blocks_subshell_dollar() {
assert!(sanitize_tool_input("foo $(id)").is_err());
}
#[test]
fn sanitize_blocks_subshell_backtick() {
assert!(sanitize_tool_input("foo `id`").is_err());
}
#[test]
fn sanitize_blocks_pipe() {
assert!(sanitize_tool_input("foo | cat").is_err());
}
#[test]
fn sanitize_blocks_semicolon() {
assert!(sanitize_tool_input("a; b").is_err());
}
#[test]
fn sanitize_blocks_newline() {
assert!(sanitize_tool_input("a\nb").is_err());
}
#[test]
fn sanitize_blocks_null_byte() {
assert!(sanitize_tool_input("a\0b").is_err());
}
#[test]
fn sanitize_allows_clean_input() {
assert!(sanitize_tool_input("hello world").is_ok());
}
#[test]
fn sanitize_allows_file_paths() {
assert!(sanitize_tool_input("/home/user/file.txt").is_ok());
}
#[test]
fn jail_allows_direct_child() {
assert!(jail_path("/project/main.rs", "/project").is_ok());
}
#[test]
fn jail_blocks_dotdot_traversal() {
assert!(jail_path("/project/foo/../../../etc/passwd", "/project").is_err());
}
#[test]
fn jail_blocks_sibling_dir() {
assert!(jail_path("/other/path", "/project").is_err());
}
#[test]
fn jail_normalises_internal_dotdot() {
let r = jail_path("/project/src/../lib/mod.rs", "/project").unwrap();
assert!(r.ends_with("lib/mod.rs"), "{r}");
}
#[test]
fn jail_normalises_curdir() {
let r = jail_path("/project/./src/main.rs", "/project").unwrap();
assert!(r.ends_with("src/main.rs"), "{r}");
}
#[test]
fn normalize_resolves_double_dotdot() {
let p = normalize(std::path::Path::new("/a/b/c/../../d"));
assert_eq!(p, std::path::PathBuf::from("/a/d"));
}
}