writeonce/reference/crates/wo-serve/src/resolve.rs

80 lines
2.3 KiB
Rust

use std::io;
use std::path::{Path, PathBuf};
/// Resolve a request path relative to a static directory.
///
/// Returns the absolute path to the file, or an error if:
/// - The path contains `..` (directory traversal)
/// - The resolved path escapes the base directory
/// - The file doesn't exist
pub fn resolve_path(base: &Path, request_path: &str) -> io::Result<PathBuf> {
// Reject any path with `..`
if request_path.contains("..") {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"path traversal rejected",
));
}
let clean = request_path.trim_start_matches('/');
let resolved = base.join(clean);
// Canonicalize and verify it's still under base.
let canonical = resolved.canonicalize().map_err(|_| {
io::Error::new(io::ErrorKind::NotFound, "file not found")
})?;
let canonical_base = base.canonicalize().map_err(|_| {
io::Error::new(io::ErrorKind::NotFound, "base directory not found")
})?;
if !canonical.starts_with(&canonical_base) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"path traversal rejected",
));
}
if !canonical.is_file() {
return Err(io::Error::new(io::ErrorKind::NotFound, "not a file"));
}
Ok(canonical)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn valid_path() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("test.css"), "body {}").unwrap();
let path = resolve_path(tmp.path(), "test.css").unwrap();
assert!(path.ends_with("test.css"));
}
#[test]
fn nested_path() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("styles")).unwrap();
fs::write(tmp.path().join("styles/main.css"), "body {}").unwrap();
let path = resolve_path(tmp.path(), "styles/main.css").unwrap();
assert!(path.ends_with("styles/main.css"));
}
#[test]
fn reject_traversal() {
let tmp = tempfile::tempdir().unwrap();
assert!(resolve_path(tmp.path(), "../etc/passwd").is_err());
}
#[test]
fn reject_nonexistent() {
let tmp = tempfile::tempdir().unwrap();
assert!(resolve_path(tmp.path(), "nonexistent.css").is_err());
}
}