use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
use crate::attacks::*;
use crate::constants::*;
use crate::encoding::*;
pub fn validate_path(path: &Path, base_dir: &Path) -> Result<PathBuf> {
let path_str = path.to_string_lossy().to_string();
let normalized_path = normalize_and_check(&path_str)?;
detect_protocol_schemes(&normalized_path)?;
detect_url_encoding(&normalized_path)?;
detect_overlong_utf8(&normalized_path)?;
detect_unicode_encoding(&normalized_path)?;
detect_dangerous_unicode(&normalized_path)?;
if path.is_absolute() {
bail!("Absolute paths are not allowed: {}", path_str);
}
detect_separator_manipulation(&normalized_path)?;
detect_advanced_traversal(&normalized_path)?;
detect_windows_attacks(&normalized_path)?;
detect_suspicious_patterns(&normalized_path)?;
validate_special_paths(&normalized_path)?;
let canonical_path = validate_path_atomic(path, base_dir)?;
Ok(canonical_path)
}
fn validate_path_atomic(path: &Path, base_dir: &Path) -> Result<PathBuf> {
let full_path = base_dir.join(path);
let canonical_base = base_dir.canonicalize()
.context("Failed to canonicalize base directory")?;
let mut visited = std::collections::HashSet::new();
let mut current_path = full_path.clone();
while current_path.is_symlink() {
if visited.contains(¤t_path) {
bail!("Recursive symlink detected: {}", current_path.display());
}
visited.insert(current_path.clone());
current_path = current_path.read_link()
.context("Failed to read symlink")?;
if visited.len() > MAX_SYMLINK_CHAIN_LENGTH {
bail!("Symlink chain too long, possible recursive symlink");
}
}
if full_path.to_string_lossy().len() > MAX_PATH_LENGTH {
bail!("Path too long: {} characters (max: {})",
full_path.to_string_lossy().len(), MAX_PATH_LENGTH);
}
let path_string = full_path.to_string_lossy();
let path_bytes = path_string.as_bytes();
for (i, &byte) in path_bytes.iter().enumerate() {
if byte == 0 {
bail!("Null byte detected at position {} in path: {}", i, full_path.display());
}
}
let path_str = full_path.to_string_lossy();
if detect_mixed_encoding(&path_str) {
bail!("Mixed encoding attack detected in path: {}", path_str);
}
let canonical_path = if full_path.exists() {
full_path.canonicalize()
.context("Failed to canonicalize existing path")?
} else {
if let Some(parent) = full_path.parent() {
let canonical_parent = parent.canonicalize()
.or_else(|_| {
if let Ok(rel_parent) = parent.strip_prefix(base_dir) {
canonical_base.join(rel_parent).canonicalize()
} else {
Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid parent path"))
}
})
.context("Failed to validate parent directory")?;
canonical_parent.join(full_path.file_name().unwrap())
} else {
bail!("Path has no parent directory: {}", full_path.display());
}
};
if !canonical_path.starts_with(&canonical_base) {
bail!(
"Path traversal detected: '{}' resolves outside base directory '{}'",
full_path.display(),
canonical_base.display()
);
}
Ok(canonical_path)
}
pub fn validate_project_name(name: &str) -> Result<String> {
if name.is_empty() {
bail!("Project name cannot be empty");
}
if name.len() > MAX_PROJECT_NAME_LENGTH {
bail!("Project name too long: {} characters (max {})", name.len(), MAX_PROJECT_NAME_LENGTH);
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
bail!("Project name contains invalid characters: {}", name);
}
if name.starts_with('-') || name.starts_with('_') || name.ends_with('-') || name.ends_with('_') {
bail!("Project name cannot start or end with '-' or '_': {}", name);
}
let name_upper = name.to_uppercase();
if WINDOWS_RESERVED_NAMES.contains(&name_upper.as_str()) {
bail!("Project name is a reserved system name: {}", name);
}
Ok(name.to_string())
}
pub fn validate_filename(filename: &str) -> Result<String> {
if filename.is_empty() {
bail!("Filename cannot be empty");
}
if filename.len() > MAX_FILENAME_LENGTH {
bail!("Filename too long: {} characters", filename.len());
}
normalize_and_check(filename)?;
detect_url_encoding(filename)?;
detect_overlong_utf8(filename)?;
detect_unicode_encoding(filename)?;
detect_dangerous_unicode(filename)?;
detect_windows_attacks(filename)?;
if filename.contains('/') || filename.contains('\\') {
bail!("Filename cannot contain path separators: {}", filename);
}
if filename == "." || filename == ".." {
bail!("Invalid filename: {}", filename);
}
if filename.contains('\0') {
bail!("Filename contains null byte");
}
if filename.chars().any(|c| c.is_control()) {
bail!("Filename contains control characters: {}", filename);
}
if filename.ends_with('.') || filename.ends_with(' ') {
bail!("Filename cannot end with dot or space: {}", filename);
}
if filename.contains(':') {
bail!("Filename cannot contain colon (NTFS stream syntax): {}", filename);
}
Ok(filename.to_string())
}