use std::path::{Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
fn normalize_unicode(input: &str) -> String {
input.nfc().collect()
}
pub fn sanitize_file_path(path: &str) -> Result<PathBuf, &'static str> {
if path.contains('\0') {
return Err("Path contains null bytes");
}
let unified_path = path.replace('\\', "/");
if unified_path.starts_with('/') || unified_path.starts_with("//") {
return Err("Absolute paths are not allowed");
}
for component in unified_path.split('/') {
if component == ".." {
return Err("Path contains directory traversal sequences");
}
}
let original_path_obj = Path::new(path);
let normalized = normalize_path(original_path_obj);
for component in normalized.components() {
if let std::path::Component::ParentDir = component {
return Err("Path attempts to escape parent directory");
}
}
Ok(normalized)
}
pub fn sanitize_file_path_internal(path: &str) -> Result<PathBuf, &'static str> {
if path.contains('\0') {
return Err("Path contains null bytes");
}
let unified_path = path.replace('\\', "/");
for component in unified_path.split('/') {
if component == ".." {
return Err("Path contains directory traversal sequences");
}
}
let original_path_obj = Path::new(path);
let normalized = normalize_path(original_path_obj);
for component in normalized.components() {
if let std::path::Component::ParentDir = component {
return Err("Path attempts to escape parent directory");
}
}
Ok(normalized)
}
pub fn is_symlink(path: &Path) -> bool {
match std::fs::symlink_metadata(path) {
Ok(meta) => meta.file_type().is_symlink(),
Err(_) => false,
}
}
pub fn resolve_and_validate_path(path: &Path, base_dir: &Path) -> Result<PathBuf, &'static str> {
if is_symlink(path) {
let resolved = std::fs::read_link(path).map_err(|_| "Cannot resolve symlink")?;
let resolved_abs = if resolved.is_absolute() {
resolved
} else {
if let Some(parent) = path.parent() {
parent.join(&resolved)
} else {
resolved
}
};
let canonical_base =
std::fs::canonicalize(base_dir).map_err(|_| "Cannot access base directory")?;
let canonical_resolved =
std::fs::canonicalize(&resolved_abs).map_err(|_| "Cannot resolve symlink target")?;
if !canonical_resolved.starts_with(&canonical_base) {
return Err("Symlink points outside allowed directory");
}
return Ok(canonical_resolved);
}
std::fs::canonicalize(path).map_err(|_| "Cannot access path")
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
if !normalized.pop() {
normalized.push(component.as_os_str());
}
}
std::path::Component::Normal(c) => {
normalized.push(c);
}
_ => {
normalized.push(component.as_os_str());
}
}
}
normalized
}
pub fn validate_environment_name(name: &str) -> Result<(), &'static str> {
let normalized = normalize_unicode(name);
if normalized.is_empty() {
return Err("Environment name cannot be empty");
}
if name.contains('\0') {
return Err("Environment name contains null bytes");
}
if !normalized
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Err(
"Environment name contains invalid characters. Only alphanumeric, underscore, and hyphen are allowed.",
);
}
if normalized.len() > 100 {
return Err("Environment name is too long (max 100 characters)");
}
Ok(())
}
pub fn validate_config_key(key: &str) -> Result<(), &'static str> {
if key.contains('\0') {
return Err("Configuration key contains null bytes");
}
let normalized = normalize_unicode(key);
if normalized.is_empty() {
return Err("Configuration key cannot be empty");
}
if !normalized
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(
"Configuration key contains invalid characters. Only alphanumeric, underscore, hyphen, and dot are allowed.",
);
}
if normalized.len() > 255 {
return Err("Configuration key is too long (max 255 characters)");
}
Ok(())
}
#[allow(dead_code)]
pub fn normalize_config_key(key: &str) -> String {
normalize_unicode(key)
}
#[allow(dead_code)]
pub fn normalize_environment_name(name: &str) -> String {
normalize_unicode(name)
}
pub fn sanitize_string_value(value: &str) -> String {
value.replace("\0", "") }
pub fn check_file_size(path: &Path, max_size: u64) -> Result<(), &'static str> {
let metadata = std::fs::metadata(path).map_err(|_| "Could not get file metadata")?;
if metadata.len() > max_size {
return Err("File exceeds maximum allowed size");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_file_path() {
assert!(sanitize_file_path("config.json").is_ok());
assert!(sanitize_file_path("./config.json").is_ok());
assert!(sanitize_file_path("folder/config.json").is_ok());
assert!(sanitize_file_path("../config.json").is_err());
assert!(sanitize_file_path("/etc/passwd").is_err());
assert!(sanitize_file_path("../../../etc/passwd").is_err());
}
#[test]
fn test_validate_environment_name() {
assert!(validate_environment_name("dev").is_ok());
assert!(validate_environment_name("production_env").is_ok());
assert!(validate_environment_name("staging-test").is_ok());
assert!(validate_environment_name("").is_err());
assert!(validate_environment_name("dev;rm -rf /").is_err());
assert!(validate_environment_name("dev/../../../etc/passwd").is_err());
}
#[test]
fn test_sanitize_file_path_advanced() {
assert!(
sanitize_file_path("folder/../config.json").is_err(),
"Should catch internal traversal"
);
assert!(sanitize_file_path("./../config.json").is_err());
assert!(
sanitize_file_path(".../config.json").is_ok(),
"Triple dot is technically a valid filename"
);
assert!(
sanitize_file_path("config.json\0.txt").is_err(),
"Null bytes are dangerous"
);
assert!(sanitize_file_path("folder\\..\\config.json").is_err());
}
#[test]
fn test_validate_environment_name_edge_cases() {
assert!(validate_environment_name(&"a".repeat(100)).is_ok());
assert!(
validate_environment_name(&"a".repeat(101)).is_err(),
"Too long"
);
assert!(
validate_environment_name("env name").is_err(),
"Spaces not allowed"
);
assert!(
validate_environment_name("env!").is_err(),
"Special chars not allowed"
);
}
#[test]
fn test_sanitize_file_path_extreme() {
assert!(sanitize_file_path("a/b/c/d/e/f/g/h/i/j/k/l/m/n/config.json").is_ok());
assert!(sanitize_file_path("my config file (1).json").is_ok());
assert!(sanitize_file_path("config-2024.01.27.json").is_ok());
assert!(sanitize_file_path("/config.json").is_err());
assert!(sanitize_file_path("//config.json").is_err());
assert!(
sanitize_file_path("~/config.json").is_ok(),
"Tilde is a literal character in this context, not expanded"
);
assert!(
sanitize_file_path("CON.json").is_ok(),
"On Linux CON is just a filename"
);
}
#[test]
fn test_validate_config_key_extreme() {
assert!(validate_config_key("a.b.c.d.e.f").is_ok());
assert!(
validate_config_key("-").is_ok(),
"Current logic allows hyphen"
);
assert!(validate_config_key("_").is_ok());
assert!(validate_config_key(".").is_ok(), "Current logic allows dot");
}
#[test]
fn test_check_file_size_logic() {
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "some data").unwrap();
assert!(check_file_size(&file_path, 100).is_ok());
assert!(
check_file_size(&file_path, 5).is_err(),
"Should fail if file is larger than max_size"
);
}
#[test]
fn test_validate_environment_name_injection() {
assert!(validate_environment_name("dev;rm -rf /").is_err());
assert!(validate_environment_name("production\n").is_err());
assert!(validate_environment_name("staging\0").is_err());
}
#[test]
fn test_sanitize_string_value_control_chars() {
let input = "val\r\n\twith\x07bell";
assert_eq!(
sanitize_string_value(input),
input,
"Control characters other than null are kept for now"
);
}
#[test]
fn test_validate_environment_name_with_unicode() {
assert!(validate_environment_name("env_🚀").is_err()); assert!(validate_environment_name("env_ümlaut").is_ok()); assert!(validate_environment_name("env_123").is_ok()); }
#[test]
fn test_validate_config_key_edge_cases() {
assert!(validate_config_key(&"a".repeat(255)).is_ok()); assert!(validate_config_key(&"a".repeat(256)).is_err()); assert!(validate_config_key("key with spaces").is_err()); assert!(validate_config_key("key!@#").is_err()); assert!(validate_config_key("valid.key-name_123").is_ok()); }
#[test]
fn test_sanitize_file_path_unicode_and_special_chars() {
assert!(sanitize_file_path("config_🚀.json").is_ok()); assert!(sanitize_file_path("file\nwith\tnewline.json").is_ok()); assert!(sanitize_file_path("file\0with_null.json").is_err()); }
#[test]
fn test_sanitize_file_path_deeply_nested_traversal() {
let deep_path = (0..100).map(|_| "dir").collect::<Vec<_>>().join("/") + "/file.txt";
assert!(sanitize_file_path(&deep_path).is_ok());
let traversal_path = (0..50).map(|_| "../").collect::<Vec<_>>().join("") + "etc/passwd";
assert!(sanitize_file_path(&traversal_path).is_err()); }
#[test]
fn test_sanitize_file_path_mixed_separators_traversal() {
assert!(sanitize_file_path("a/b\\c/../d").is_err()); assert!(sanitize_file_path("a/b\\c/../../d").is_err()); assert!(sanitize_file_path("a/b\\c/d").is_ok()); }
#[test]
fn test_validate_environment_name_unicode_normalization() {
assert!(validate_environment_name("аpple").is_ok()); assert!(validate_environment_name("test-env_123").is_ok()); }
#[test]
fn test_sanitize_file_path_url_encoded_traversal() {
assert!(sanitize_file_path("config%2Ejson").is_ok()); assert!(sanitize_file_path("normal/config.json").is_ok()); }
#[test]
fn test_validate_config_key_with_unicode() {
assert!(validate_config_key("key_🚀").is_err()); assert!(validate_config_key("key_ümlaut").is_ok()); assert!(validate_config_key("valid.key-name_123").is_ok()); }
#[test]
fn test_sanitize_file_path_symlink_like_patterns() {
assert!(sanitize_file_path("symlink_target").is_ok());
assert!(sanitize_file_path("link->target").is_ok()); assert!(sanitize_file_path("../link->target").is_err()); }
#[test]
fn test_validate_config_key_with_extreme_unicode() {
assert!(validate_config_key("key_αβγδε").is_ok()); assert!(validate_config_key("key_Здравствуйте").is_ok()); assert!(validate_config_key("key_🚀").is_err()); assert!(validate_config_key("key_café_naïve").is_ok()); }
#[test]
fn test_validate_environment_name_with_extreme_cases() {
let max_len_name = "a".repeat(100);
assert!(validate_environment_name(&max_len_name).is_ok());
let over_limit_name = "a".repeat(101);
assert!(validate_environment_name(&over_limit_name).is_err());
assert!(validate_environment_name("dev_env-test_123").is_ok());
assert!(validate_environment_name("dev env").is_err()); assert!(validate_environment_name("dev/env").is_err()); assert!(validate_environment_name("dev\\env").is_err()); assert!(validate_environment_name("dev@env").is_err()); }
#[test]
fn test_sanitize_file_path_with_extreme_unicode() {
assert!(sanitize_file_path("config_🚀.json").is_ok());
assert!(sanitize_file_path("file_αβγ.txt").is_ok());
assert!(sanitize_file_path("你好世界.cfg").is_ok());
assert!(sanitize_file_path("config_🚀/../passwd").is_err());
assert!(sanitize_file_path("αβγ/../file.txt").is_err());
}
#[test]
fn test_sanitize_file_path_with_percent_encoding() {
assert!(sanitize_file_path("config%2Ejson").is_ok()); assert!(sanitize_file_path("%2E%2E%2Fetc%2Fpasswd").is_ok()); assert!(sanitize_file_path("normal/file.txt").is_ok());
}
#[test]
fn test_sanitize_file_path_with_alternative_encodings() {
assert!(sanitize_file_path("config.json").is_ok());
assert!(sanitize_file_path("..%2ftest").is_ok()); assert!(sanitize_file_path("..%5Ctest").is_ok()); }
#[test]
fn test_validate_config_key_with_edge_character_combinations() {
assert!(validate_config_key("a_b.c-d").is_ok()); assert!(validate_config_key("a..b").is_ok()); assert!(validate_config_key("a__b").is_ok()); assert!(validate_config_key("a--b").is_ok()); assert!(validate_config_key("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z").is_ok());
assert!(validate_config_key("a@b").is_err()); assert!(validate_config_key("a!b").is_err()); assert!(validate_config_key("a b").is_err()); assert!(validate_config_key("a[b]").is_err()); }
#[test]
fn test_sanitize_string_value_with_extreme_cases() {
let input_with_controls =
"value\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F";
let sanitized = sanitize_string_value(input_with_controls);
assert!(!sanitized.contains('\0')); assert!(sanitized.contains('\x01'));
assert!(sanitized.contains('\x02'));
assert!(sanitized.contains('\t')); assert!(sanitized.contains('\n'));
let unicode_input = "value_with_🚀_and_üñíçødé";
let sanitized_unicode = sanitize_string_value(unicode_input);
assert_eq!(sanitized_unicode, unicode_input); }
#[test]
fn test_validate_environment_name_with_homoglyphs() {
assert!(validate_environment_name("аpple").is_ok()); assert!(validate_environment_name("аdmin").is_ok()); assert!(validate_environment_name("test-env_123").is_ok()); }
#[test]
fn test_sanitize_file_path_with_mixed_encoding_tricks() {
assert!(sanitize_file_path("././config.json").is_ok()); assert!(sanitize_file_path("./../config.json").is_err()); assert!(sanitize_file_path(".../config.json").is_ok()); assert!(sanitize_file_path("....//config.json").is_ok()); assert!(sanitize_file_path("config.json.").is_ok()); assert!(sanitize_file_path("config.json..").is_ok()); }
#[test]
fn test_check_file_size_with_edge_cases() {
let temp_dir = tempfile::TempDir::new().unwrap();
let exact_size_file = temp_dir.path().join("exact_size.txt");
std::fs::write(&exact_size_file, &vec![0u8; 1000]).unwrap();
assert!(check_file_size(&exact_size_file, 1000).is_ok());
let over_size_file = temp_dir.path().join("over_size.txt");
std::fs::write(&over_size_file, &vec![0u8; 1001]).unwrap();
assert!(check_file_size(&over_size_file, 1000).is_err());
let zero_size_file = temp_dir.path().join("zero_size.txt");
std::fs::write(&zero_size_file, "").unwrap();
assert!(check_file_size(&zero_size_file, 1000).is_ok());
let non_existent = temp_dir.path().join("non_existent.txt");
assert!(check_file_size(&non_existent, 1000).is_err());
}
#[test]
fn test_sanitize_file_path_with_extreme_path_depth() {
let deep_path = (0..1000).map(|_| "dir").collect::<Vec<_>>().join("/") + "/file.txt";
assert!(sanitize_file_path(&deep_path).is_ok()); }
#[test]
fn test_sanitize_file_path_with_extreme_traversal_attempts() {
let traversal_path = (0..500).map(|_| "../").collect::<Vec<_>>().join("") + "etc/passwd";
assert!(sanitize_file_path(&traversal_path).is_err()); }
#[test]
fn test_validate_environment_name_with_extreme_length() {
let max_len_name = "a".repeat(100);
assert!(validate_environment_name(&max_len_name).is_ok());
let over_limit_name = "a".repeat(101);
assert!(validate_environment_name(&over_limit_name).is_err());
assert!(validate_environment_name("").is_err());
assert!(validate_environment_name("a").is_ok());
}
#[test]
fn test_validate_config_key_with_extreme_length() {
let max_len_key = "a".repeat(255);
assert!(validate_config_key(&max_len_key).is_ok());
let over_limit_key = "a".repeat(256);
assert!(validate_config_key(&over_limit_key).is_err());
assert!(validate_config_key("").is_err());
assert!(validate_config_key("a").is_ok());
}
#[test]
fn test_validate_config_key_with_all_allowed_characters() {
let mixed_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.";
assert!(validate_config_key(mixed_key).is_ok());
assert!(validate_config_key("a_0.z-9").is_ok());
}
#[test]
fn test_validate_config_key_with_various_invalid_characters() {
assert!(validate_config_key("key@value").is_err()); assert!(validate_config_key("key!value").is_err()); assert!(validate_config_key("key#value").is_err()); assert!(validate_config_key("key%value").is_err()); assert!(validate_config_key("key^value").is_err()); assert!(validate_config_key("key&value").is_err()); assert!(validate_config_key("key*value").is_err()); assert!(validate_config_key("key(value").is_err()); assert!(validate_config_key("key)value").is_err()); assert!(validate_config_key("key[value").is_err()); assert!(validate_config_key("key]value").is_err()); assert!(validate_config_key("key{value").is_err()); assert!(validate_config_key("key}value").is_err()); assert!(validate_config_key("key|value").is_err()); assert!(validate_config_key("key\\value").is_err()); assert!(validate_config_key("key/value").is_err()); assert!(validate_config_key("key:value").is_err()); assert!(validate_config_key("key;value").is_err()); assert!(validate_config_key("key'value").is_err()); assert!(validate_config_key("key\"value").is_err()); assert!(validate_config_key("key<value").is_err()); assert!(validate_config_key("key>value").is_err()); assert!(validate_config_key("key,value").is_err()); assert!(validate_config_key("key?value").is_err()); assert!(validate_config_key("key space").is_err()); }
#[test]
fn test_sanitize_string_value_with_extreme_control_sequences() {
let input_with_controls = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
let sanitized = sanitize_string_value(input_with_controls);
assert!(!sanitized.contains('\0')); assert_eq!(sanitized.chars().count(), 31); }
#[test]
fn test_validate_environment_name_with_unicode_confusables() {
assert!(validate_environment_name("аdmin").is_ok()); assert!(validate_environment_name("аpple").is_ok()); assert!(validate_environment_name("admin").is_ok()); assert!(validate_environment_name("рhish").is_ok()); }
#[test]
fn test_sanitize_file_path_with_multiple_encodings() {
assert!(sanitize_file_path("normal/path/file.txt").is_ok());
assert!(sanitize_file_path("path/with/special_chars.txt").is_ok());
assert!(sanitize_file_path("path/with spaces/file.txt").is_ok());
assert!(sanitize_file_path("path/with.123/file.txt").is_ok());
assert!(sanitize_file_path("path/with-dashes/file.txt").is_ok());
assert!(sanitize_file_path("path/with_underscores/file.txt").is_ok());
}
}