use std::path::{Component, Path, PathBuf};
use super::types::{FxpStatus, SftpError};
pub fn lexically_clean(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
out.push("/");
for comp in p.components() {
match comp {
Component::Prefix(_) | Component::RootDir | Component::CurDir => {}
Component::ParentDir => {
if out.parent().is_some() {
out.pop();
}
}
Component::Normal(c) => out.push(c),
}
}
out
}
pub fn resolve(cwd: &Path, raw: &[u8], root: Option<&Path>) -> Result<PathBuf, SftpError> {
let s = std::str::from_utf8(raw).map_err(|_| SftpError::status(FxpStatus::BadMessage))?;
let p = Path::new(s);
let joined = if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
};
let clean = lexically_clean(&joined);
if let Some(jail) = root {
let jail_clean = lexically_clean(jail);
if !is_inside(&clean, &jail_clean) {
return Err(SftpError::status_msg(
FxpStatus::PermissionDenied,
"path escapes jail root",
));
}
}
Ok(clean)
}
pub fn is_inside(child: &Path, parent: &Path) -> bool {
let mut c = child.components();
let mut p = parent.components();
loop {
match (c.next(), p.next()) {
(Some(ca), Some(pa)) if ca == pa => continue,
(_, None) => return true,
_ => return false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lexically_clean_simple() {
assert_eq!(lexically_clean(Path::new("/a/b/./c")), Path::new("/a/b/c"));
assert_eq!(
lexically_clean(Path::new("/a/b/../c/./d")),
Path::new("/a/c/d")
);
assert_eq!(lexically_clean(Path::new("/../..")), Path::new("/"));
assert_eq!(lexically_clean(Path::new("/a//b///c")), Path::new("/a/b/c"));
}
#[test]
fn resolve_absolute_replaces_cwd() {
let p = resolve(Path::new("/home/u"), b"/etc/passwd", None).unwrap();
assert_eq!(p, Path::new("/etc/passwd"));
}
#[test]
fn resolve_relative_joins_cwd() {
let p = resolve(Path::new("/home/u"), b"foo/bar", None).unwrap();
assert_eq!(p, Path::new("/home/u/foo/bar"));
}
#[test]
fn resolve_dotdot_traversal() {
let p = resolve(Path::new("/tmp/x/y"), b"../../etc/passwd", None).unwrap();
assert_eq!(p, Path::new("/tmp/etc/passwd"));
let p = resolve(Path::new("/tmp/x/y"), b"../../../etc/passwd", None).unwrap();
assert_eq!(p, Path::new("/etc/passwd"));
}
#[test]
fn resolve_jail_blocks_escape() {
let err = resolve(
Path::new("/srv/jail/u"),
b"../../etc/passwd",
Some(Path::new("/srv/jail")),
)
.unwrap_err();
match err {
SftpError::Status { code, .. } => assert_eq!(code, FxpStatus::PermissionDenied),
_ => panic!("expected PermissionDenied"),
}
}
#[test]
fn resolve_jail_allows_inside() {
let p = resolve(
Path::new("/srv/jail/u"),
b"file.txt",
Some(Path::new("/srv/jail")),
)
.unwrap();
assert_eq!(p, Path::new("/srv/jail/u/file.txt"));
}
#[test]
fn is_inside_basics() {
assert!(is_inside(Path::new("/a/b/c"), Path::new("/a/b")));
assert!(is_inside(Path::new("/a/b"), Path::new("/a/b")));
assert!(is_inside(Path::new("/a/b/c"), Path::new("/")));
assert!(!is_inside(Path::new("/a/bc"), Path::new("/a/b")));
assert!(!is_inside(Path::new("/a"), Path::new("/a/b")));
}
}