path validation
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Validates and sanitizes a relative path to prevent directory traversal attacks.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the path attempts to escape the base directory or contains invalid components.
|
||||
pub fn validate_relative_path(base_dir: &Path, relative_path: &str) -> eyre::Result<PathBuf> {
|
||||
// Create the full path by joining base directory with relative path
|
||||
let full_path = base_dir.join(relative_path);
|
||||
|
||||
// Normalize the path to resolve any ".." or "." components
|
||||
let canonical_path = match std::fs::canonicalize(&full_path) {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
// If the file doesn't exist, we can't canonicalize it.
|
||||
// Instead, we'll manually validate the path components.
|
||||
validate_path_components(&full_path, base_dir)?
|
||||
}
|
||||
};
|
||||
|
||||
// Get the canonical base directory
|
||||
let canonical_base = match std::fs::canonicalize(base_dir) {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
// If base directory doesn't exist, use its absolute form
|
||||
match std::fs::canonicalize(base_dir) {
|
||||
Ok(path) => path,
|
||||
Err(_) => base_dir.to_path_buf(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the canonical path starts with the canonical base directory
|
||||
if !canonical_path.starts_with(&canonical_base) {
|
||||
eyre::bail!(
|
||||
"Path validation failed: {} attempts to escape base directory",
|
||||
relative_path
|
||||
);
|
||||
}
|
||||
|
||||
Ok(canonical_path)
|
||||
}
|
||||
|
||||
/// Validates path components without requiring the file to exist
|
||||
fn validate_path_components(full_path: &Path, base_dir: &Path) -> eyre::Result<PathBuf> {
|
||||
let mut result = base_dir.to_path_buf();
|
||||
|
||||
for component in full_path.components() {
|
||||
match component {
|
||||
std::path::Component::Prefix(_) => {
|
||||
eyre::bail!("Path contains Windows prefix: {:?}", full_path);
|
||||
}
|
||||
std::path::Component::RootDir | std::path::Component::CurDir => {
|
||||
// Skip root directory and current directory components
|
||||
}
|
||||
std::path::Component::ParentDir => {
|
||||
// Check if we can go up one level without escaping base_dir
|
||||
if !result.pop() {
|
||||
eyre::bail!(
|
||||
"Path attempts to escape base directory with '..': {:?}",
|
||||
full_path
|
||||
);
|
||||
}
|
||||
}
|
||||
std::path::Component::Normal(name) => {
|
||||
// Validate the component name
|
||||
let name_str = name.to_string_lossy();
|
||||
if name_str.contains('\0') || name_str.contains('/') || name_str.contains('\\') {
|
||||
eyre::bail!("Path component contains invalid characters: {}", name_str);
|
||||
}
|
||||
result.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Validates a relative path that will be used for accessing files within a game directory.
|
||||
/// This is a stricter validation that ensures the path is relative and doesn't escape.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the path is absolute or attempts directory traversal.
|
||||
pub fn validate_game_file_path(game_dir: &Path, relative_path: &str) -> eyre::Result<PathBuf> {
|
||||
// First check if the relative path is actually relative
|
||||
if Path::new(relative_path).is_absolute() {
|
||||
eyre::bail!("Path must be relative, not absolute: {}", relative_path);
|
||||
}
|
||||
|
||||
// Check for obvious traversal attempts
|
||||
if relative_path.contains("..") {
|
||||
eyre::bail!("Path contains directory traversal: {}", relative_path);
|
||||
}
|
||||
|
||||
// For Windows paths
|
||||
if relative_path.contains(':')
|
||||
&& (relative_path.contains(":\\") || relative_path.contains(":/"))
|
||||
{
|
||||
eyre::bail!("Path contains drive letter: {}", relative_path);
|
||||
}
|
||||
|
||||
// Use the general validation function
|
||||
validate_relative_path(game_dir, relative_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_temp_dir() -> std::io::Result<std::path::PathBuf> {
|
||||
let mut dir = std::env::temp_dir();
|
||||
dir.push(format!("lanspread_test_{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
struct TempDir(std::path::PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> std::io::Result<Self> {
|
||||
let path = create_temp_dir()?;
|
||||
Ok(TempDir(path))
|
||||
}
|
||||
|
||||
fn path(&self) -> &std::path::Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_paths() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
|
||||
let base = temp_dir.path();
|
||||
|
||||
// Valid relative paths
|
||||
assert!(validate_game_file_path(base, "file.txt").is_ok());
|
||||
assert!(validate_game_file_path(base, "subdir/file.txt").is_ok());
|
||||
assert!(validate_game_file_path(base, "subdir/deep/nested/file.txt").is_ok());
|
||||
assert!(validate_game_file_path(base, "file with spaces.txt").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_traversal_attempts() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
|
||||
let base = temp_dir.path();
|
||||
|
||||
// These should all fail
|
||||
assert!(validate_game_file_path(base, "../outside.txt").is_err());
|
||||
assert!(validate_game_file_path(base, "subdir/../../outside.txt").is_err());
|
||||
assert!(validate_game_file_path(base, "../../etc/passwd").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_absolute_paths() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
|
||||
let base = temp_dir.path();
|
||||
|
||||
// Absolute paths should fail
|
||||
assert!(validate_game_file_path(base, "/etc/passwd").is_err());
|
||||
if cfg!(windows) {
|
||||
assert!(validate_game_file_path(base, "C:\\Windows\\System32\\cmd.exe").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_windows_specific() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
|
||||
let base = temp_dir.path();
|
||||
|
||||
// Windows-specific paths that should fail
|
||||
assert!(validate_game_file_path(base, "C:\\evil.txt").is_err());
|
||||
assert!(validate_game_file_path(base, "D:/evil.txt").is_err());
|
||||
assert!(validate_game_file_path(base, "\\\\server\\share\\file.txt").is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user