use displaydoc::Display;
use super::DiagnosticsError;
#[derive(Debug, thiserror::Error, Display)]
pub(super) enum SecurityError {
PathTraversal { filename: String },
InvalidFileType {
filename: String,
allowed_extensions: Vec<String>,
},
FileNotFound { filename: String },
}
impl From<SecurityError> for DiagnosticsError {
fn from(error: SecurityError) -> Self {
DiagnosticsError::Internal(error.to_string())
}
}
pub(super) struct SecurityValidator;
impl SecurityValidator {
pub(super) fn validate_filename(filename: &str) -> Result<(), SecurityError> {
if filename.contains("..") || filename.contains('/') || filename.contains('\\') {
return Err(SecurityError::PathTraversal {
filename: filename.to_string(),
});
}
Ok(())
}
pub(super) fn validate_file_extension(
filename: &str,
allowed_extensions: &[&str],
) -> Result<(), SecurityError> {
let has_valid_extension = allowed_extensions.iter().any(|ext| filename.ends_with(ext));
if !has_valid_extension {
return Err(SecurityError::InvalidFileType {
filename: filename.to_string(),
allowed_extensions: allowed_extensions.iter().map(|s| s.to_string()).collect(),
});
}
Ok(())
}
pub(super) fn validate_memory_dump_filename(filename: &str) -> Result<(), SecurityError> {
Self::validate_filename(filename)?;
Self::validate_file_extension(filename, &[".prof"])?;
Ok(())
}
pub(super) fn validate_file_exists_and_is_file<P: AsRef<std::path::Path>>(
path: P,
filename: &str,
) -> Result<(), SecurityError> {
let path = path.as_ref();
if !path.exists() || !path.is_file() {
return Err(SecurityError::FileNotFound {
filename: filename.to_string(),
});
}
Ok(())
}
pub(super) fn validate_file_download(
file_path: &std::path::Path,
filename: &str,
allowed_extensions: &[&str],
) -> Result<(), SecurityError> {
Self::validate_filename(filename)?;
Self::validate_file_extension(filename, allowed_extensions)?;
Self::validate_file_exists_and_is_file(file_path, filename)?;
Ok(())
}
pub(super) fn validate_file_deletion(
file_path: &std::path::Path,
filename: &str,
allowed_extensions: &[&str],
) -> Result<(), SecurityError> {
Self::validate_file_download(file_path, filename, allowed_extensions)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn test_validate_filename_safe() {
assert!(SecurityValidator::validate_filename("test.prof").is_ok());
assert!(SecurityValidator::validate_filename("router_heap_dump_1234567890.prof").is_ok());
assert!(SecurityValidator::validate_filename("simple_file.txt").is_ok());
}
#[test]
fn test_validate_filename_path_traversal() {
assert!(SecurityValidator::validate_filename("../test.prof").is_err());
assert!(SecurityValidator::validate_filename("test/../file.prof").is_err());
assert!(SecurityValidator::validate_filename("..\\test.prof").is_err());
assert!(SecurityValidator::validate_filename("path/to/file.prof").is_err());
assert!(SecurityValidator::validate_filename("path\\to\\file.prof").is_err());
}
#[test]
fn test_validate_file_extension() {
assert!(SecurityValidator::validate_file_extension("test.prof", &[".prof"]).is_ok());
assert!(SecurityValidator::validate_file_extension("test.txt", &[".txt", ".log"]).is_ok());
assert!(SecurityValidator::validate_file_extension("test.exe", &[".prof"]).is_err());
assert!(SecurityValidator::validate_file_extension("test.prof", &[".txt"]).is_err());
assert!(SecurityValidator::validate_file_extension("test", &[".prof"]).is_err());
}
#[test]
fn test_validate_memory_dump_filename() {
assert!(
SecurityValidator::validate_memory_dump_filename("router_heap_dump_1234.prof").is_ok()
);
assert!(SecurityValidator::validate_memory_dump_filename("../test.prof").is_err());
assert!(SecurityValidator::validate_memory_dump_filename("test.exe").is_err());
assert!(SecurityValidator::validate_memory_dump_filename("path/test.prof").is_err());
}
#[test]
fn test_validate_file_exists_and_is_file() {
let nonexistent = Path::new("/path/that/does/not/exist/file.prof");
assert!(
SecurityValidator::validate_file_exists_and_is_file(nonexistent, "file.prof").is_err()
);
let current_dir = Path::new(".");
if current_dir.exists() && current_dir.is_dir() {
assert!(
SecurityValidator::validate_file_exists_and_is_file(current_dir, "directory")
.is_err()
);
}
use std::fs::File;
use tempfile::tempdir;
let temp_dir = tempdir().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test.prof");
File::create(&temp_file).expect("Failed to create temp file");
assert!(
SecurityValidator::validate_file_exists_and_is_file(&temp_file, "test.prof").is_ok()
);
}
#[test]
fn test_security_error_types() {
let path_traversal = SecurityError::PathTraversal {
filename: "../test".to_string(),
};
let invalid_type = SecurityError::InvalidFileType {
filename: "test.exe".to_string(),
allowed_extensions: vec![".prof".to_string()],
};
let not_found = SecurityError::FileNotFound {
filename: "missing.prof".to_string(),
};
assert!(matches!(
path_traversal,
SecurityError::PathTraversal { .. }
));
assert!(matches!(
invalid_type,
SecurityError::InvalidFileType { .. }
));
assert!(matches!(not_found, SecurityError::FileNotFound { .. }));
}
}