use regex::Regex;
use std::collections::HashMap;
use std::sync::OnceLock;
fn compile_pattern(pattern: &str) -> Result<Regex, EnvSecurityError> {
Regex::new(pattern).map_err(|e| EnvSecurityError::InvalidRegex {
pattern: pattern.to_string(),
error: e.to_string(),
})
}
fn get_allowed_patterns() -> Result<&'static Vec<Regex>, EnvSecurityError> {
static ALLOWED_PATTERNS: OnceLock<Result<Vec<Regex>, EnvSecurityError>> = OnceLock::new();
match ALLOWED_PATTERNS.get_or_init(|| {
vec![
compile_pattern(r"^[A-Z][A-Z0-9_]*$"),
compile_pattern(r"^[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*$"),
compile_pattern(r"^[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*$"),
]
.into_iter()
.collect()
}) {
Ok(patterns) => Ok(patterns),
Err(e) => Err(e.clone()),
}
}
fn get_blocked_patterns() -> Result<&'static Vec<Regex>, EnvSecurityError> {
static BLOCKED_PATTERNS: OnceLock<Result<Vec<Regex>, EnvSecurityError>> = OnceLock::new();
match BLOCKED_PATTERNS.get_or_init(|| {
vec![
compile_pattern(r"(?i)^(PATH|LD_LIBRARY_PATH|LD_PRELOAD)$"),
compile_pattern(r"(?i)^(SHELL|HOME|USER|LOGNAME)$"),
compile_pattern(r"(?i)^(PWD|OLDPWD)$"),
compile_pattern(r"(?i)^(MAIL|MAILCHECK)$"),
compile_pattern(r"(?i)^(TERM|TERMCAP)$"),
compile_pattern(r"(?i)^(DISPLAY|XAUTHORITY)$"),
compile_pattern(r"(?i)^(SSH_AUTH_SOCK|SSH_AGENT_PID)$"),
compile_pattern(r"(?i)^(DOCKER_HOST|KUBECONFIG)$"),
compile_pattern(r"(?i).*(_SECRET|_PASSWORD|_TOKEN|_KEY|_PRIVATE)$"),
compile_pattern(r".*[;<>&|`$].*"),
compile_pattern(r"^BASH_FUNC_.*"),
]
.into_iter()
.collect()
}) {
Ok(patterns) => Ok(patterns),
Err(e) => Err(e.clone()),
}
}
fn get_allowed_pattern_strings() -> &'static Vec<&'static str> {
static ALLOWED_PATTERNS_STR: OnceLock<Vec<&'static str>> = OnceLock::new();
ALLOWED_PATTERNS_STR.get_or_init(|| {
vec![
r"^[A-Z][A-Z0-9_]*$",
r"^[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*$",
r"^[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*_[A-Z][A-Z0-9_]*$",
]
})
}
#[derive(Debug, Clone)]
pub struct EnvironmentValidationConfig {
max_name_length: usize,
max_value_length: usize,
enable_blocked_patterns: bool,
enable_length_validation: bool,
allow_encrypted_values: bool,
blocked_patterns: Vec<String>,
allowed_patterns: Vec<String>,
}
impl Default for EnvironmentValidationConfig {
fn default() -> Self {
Self::new()
}
}
impl EnvironmentValidationConfig {
pub fn new() -> Self {
Self {
max_name_length: 256,
max_value_length: 4096,
enable_blocked_patterns: true,
enable_length_validation: true,
allow_encrypted_values: true,
blocked_patterns: Vec::new(),
allowed_patterns: Vec::new(),
}
}
pub fn with_max_name_length(mut self, length: usize) -> Self {
self.max_name_length = length;
self
}
pub fn with_max_value_length(mut self, length: usize) -> Self {
self.max_value_length = length;
self
}
pub fn with_blocked_patterns_check(mut self, enabled: bool) -> Self {
self.enable_blocked_patterns = enabled;
self
}
pub fn with_length_validation(mut self, enabled: bool) -> Self {
self.enable_length_validation = enabled;
self
}
pub fn with_blocked_patterns_disabled(mut self) -> Self {
self.enable_blocked_patterns = false;
self
}
pub fn with_length_validation_disabled(mut self) -> Self {
self.enable_length_validation = false;
self
}
pub fn with_custom_blocked_patterns(mut self, patterns: Vec<String>) -> Self {
self.blocked_patterns = patterns;
self
}
pub fn with_custom_allowed_patterns(mut self, patterns: Vec<String>) -> Self {
self.allowed_patterns = patterns;
self
}
pub fn with_encrypted_values(mut self, allow: bool) -> Self {
self.allow_encrypted_values = allow;
self
}
pub fn max_name_length(&self) -> usize {
self.max_name_length
}
pub fn max_value_length(&self) -> usize {
self.max_value_length
}
}
#[derive(Debug, Clone)]
pub struct EnvSecurityValidator {
max_name_length: usize,
max_value_length: usize,
config: EnvironmentValidationConfig,
}
impl Default for EnvSecurityValidator {
fn default() -> Self {
Self::new()
}
}
impl EnvSecurityValidator {
pub fn new() -> Self {
Self::with_config(EnvironmentValidationConfig::new())
}
pub fn with_config(config: EnvironmentValidationConfig) -> Self {
Self {
max_name_length: config.max_name_length,
max_value_length: config.max_value_length,
config,
}
}
pub fn strict() -> Self {
Self::with_config(
EnvironmentValidationConfig::new()
.with_max_name_length(128)
.with_max_value_length(2048),
)
}
pub fn lenient() -> Self {
Self::with_config(
EnvironmentValidationConfig::new()
.with_max_name_length(1024)
.with_max_value_length(8192)
.with_blocked_patterns_disabled()
.with_length_validation_disabled(),
)
}
pub fn validate_env_name(
&self,
name: &str,
value: Option<&str>,
) -> Result<(), EnvSecurityError> {
let blocked_patterns = get_blocked_patterns()?;
let allowed_patterns = get_allowed_patterns()?;
if self.config.enable_length_validation && name.len() > self.max_name_length {
return Err(EnvSecurityError::NameTooLong {
name: name.to_string(),
max_length: self.max_name_length,
actual_length: name.len(),
});
}
if self.config.enable_blocked_patterns {
for pattern in blocked_patterns {
if pattern.is_match(name) {
if let Some(val) = value {
if self.config.allow_encrypted_values && val.starts_with("enc:") {
continue;
}
}
return Err(EnvSecurityError::BlockedName {
name: name.to_string(),
pattern: pattern.as_str().to_string(),
});
}
}
}
let mut matched = false;
for pattern in allowed_patterns {
if pattern.is_match(name) {
matched = true;
break;
}
}
if !matched {
return Err(EnvSecurityError::InvalidNameFormat {
name: name.to_string(),
expected_patterns: get_allowed_pattern_strings()
.iter()
.map(|s| s.to_string())
.collect(),
});
}
Ok(())
}
pub fn validate_env_name_simple(&self, name: &str) -> Result<(), EnvSecurityError> {
self.validate_env_name(name, None)
}
pub fn validate_env_value(&self, value: &str) -> Result<(), EnvSecurityError> {
if self.config.allow_encrypted_values && value.starts_with("enc:") {
return Ok(());
}
if self.config.enable_length_validation && value.len() > self.max_value_length {
return Err(EnvSecurityError::ValueTooLong {
value_length: value.len(),
max_length: self.max_value_length,
});
}
if self.config.enable_blocked_patterns {
if value
.chars()
.any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t')
{
return Err(EnvSecurityError::CommandInjection {
pattern: "control_character".to_string(),
});
}
if value.contains('\0') {
return Err(EnvSecurityError::NullByte);
}
if value.contains("${") && value.contains('}') {
return Err(EnvSecurityError::ShellExpansion);
}
let dangerous_patterns = [
";", "&", "|", "`", "$", "(", ")", "<", ">", "\n", "\r", "\\",
"\t", "\\n", "\\r", "\\t", "; ", "& ", "| ", "$ ", ];
for pattern in &dangerous_patterns {
if value.contains(pattern) {
return Err(EnvSecurityError::CommandInjection {
pattern: pattern.to_string(),
});
}
}
}
Ok(())
}
pub fn validate_env_mapping(
&self,
mapping: &HashMap<String, String>,
) -> Result<(), EnvSecurityError> {
for (field_name, env_name) in mapping {
self.validate_env_name_simple(env_name)?;
if field_name.is_empty() || field_name.contains(' ') {
return Err(EnvSecurityError::InvalidFieldName {
field_name: field_name.to_string(),
});
}
}
Ok(())
}
pub fn sanitize_for_logging(&self, value: &str) -> String {
if value.len() > 100 {
format!("{}...", &value[..97])
} else {
value.to_string()
}
}
pub fn should_allow_env_var(&self, name: &str) -> bool {
self.validate_env_name_simple(name).is_ok()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnvSecurityError {
NameTooLong {
name: String,
max_length: usize,
actual_length: usize,
},
BlockedName { name: String, pattern: String },
InvalidNameFormat {
name: String,
expected_patterns: Vec<String>,
},
InvalidRegex { pattern: String, error: String },
ValueTooLong {
value_length: usize,
max_length: usize,
},
NullByte,
ShellExpansion,
CommandInjection { pattern: String },
InvalidFieldName { field_name: String },
}
impl std::fmt::Display for EnvSecurityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnvSecurityError::NameTooLong {
name,
max_length,
actual_length,
} => {
write!(
f,
"Environment variable name '{}' is too long: {} > {}",
name, actual_length, max_length
)
}
EnvSecurityError::BlockedName { name, pattern } => {
write!(
f,
"Environment variable name '{}' matches blocked pattern: {}",
name, pattern
)
}
EnvSecurityError::InvalidNameFormat {
name,
expected_patterns,
} => {
write!(
f,
"Environment variable name '{}' doesn't match any allowed pattern: {:?}",
name, expected_patterns
)
}
EnvSecurityError::InvalidRegex { pattern, error } => {
write!(f, "Invalid regex pattern '{}': {}", pattern, error)
}
EnvSecurityError::ValueTooLong {
value_length,
max_length,
} => {
write!(
f,
"Environment variable value is too long: {} > {}",
value_length, max_length
)
}
EnvSecurityError::NullByte => {
write!(f, "Environment variable value contains null bytes")
}
EnvSecurityError::ShellExpansion => {
write!(f, "Environment variable value contains shell expansion")
}
EnvSecurityError::CommandInjection { pattern } => {
write!(
f,
"Environment variable value contains dangerous pattern: '{}'",
pattern
)
}
EnvSecurityError::InvalidFieldName { field_name } => {
write!(
f,
"Invalid field name in environment mapping: '{}'",
field_name
)
}
}
}
}
impl std::error::Error for EnvSecurityError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_valid_env_name() {
let validator = EnvSecurityValidator::new();
assert!(validator.validate_env_name_simple("APP_PORT").is_ok());
assert!(validator.validate_env_name_simple("DATABASE_HOST").is_ok());
assert!(validator.validate_env_name_simple("REDIS_PORT").is_ok());
}
#[test]
fn test_validate_blocked_env_name() {
let validator = EnvSecurityValidator::new();
assert!(validator.validate_env_name_simple("PATH").is_err());
assert!(validator.validate_env_name_simple("HOME").is_err());
assert!(validator.validate_env_name_simple("SECRET_KEY").is_err());
assert!(validator.validate_env_name_simple("API_TOKEN").is_err());
}
#[test]
fn test_validate_invalid_env_name_format() {
let validator = EnvSecurityValidator::new();
assert!(validator.validate_env_name_simple("app_port").is_err()); assert!(validator.validate_env_name_simple("APP-PORT").is_err()); assert!(validator.validate_env_name_simple("123PORT").is_err()); }
#[test]
fn test_validate_env_name_length() {
let validator = EnvSecurityValidator::new();
let valid_256 = "A".repeat(256);
assert!(validator.validate_env_name_simple(&valid_256).is_ok());
let valid_255 = "A".repeat(255);
assert!(validator.validate_env_name_simple(&valid_255).is_ok());
let invalid_257 = "A".repeat(257);
assert!(validator.validate_env_name_simple(&invalid_257).is_err());
}
#[test]
fn test_validate_dangerous_env_value() {
let validator = EnvSecurityValidator::new();
assert!(validator.validate_env_value("hello").is_ok());
assert!(validator.validate_env_value("test123").is_ok());
assert!(validator.validate_env_value("hello;world").is_err()); assert!(validator.validate_env_value("hello|world").is_err()); assert!(validator.validate_env_value("hello${world}").is_err()); assert!(validator.validate_env_value("hello\0world").is_err()); }
#[test]
fn test_validate_env_mapping() {
let validator = EnvSecurityValidator::new();
let mut mapping = HashMap::new();
mapping.insert("port".to_string(), "APP_PORT".to_string());
mapping.insert("host".to_string(), "DATABASE_HOST".to_string());
assert!(validator.validate_env_mapping(&mapping).is_ok());
let mut bad_mapping = HashMap::new();
bad_mapping.insert("invalid field".to_string(), "APP_PORT".to_string());
assert!(validator.validate_env_mapping(&bad_mapping).is_err());
let mut bad_env_mapping = HashMap::new();
bad_env_mapping.insert("port".to_string(), "PATH".to_string());
assert!(validator.validate_env_mapping(&bad_env_mapping).is_err());
}
#[test]
fn test_custom_length_limits() {
let config = EnvironmentValidationConfig::new()
.with_max_name_length(100)
.with_max_value_length(500);
let validator = EnvSecurityValidator::with_config(config);
let valid_100 = "A".repeat(100);
assert!(validator.validate_env_name_simple(&valid_100).is_ok());
let invalid_101 = "A".repeat(101);
assert!(validator.validate_env_name_simple(&invalid_101).is_err());
assert!(validator.validate_env_value(&"x".repeat(500)).is_ok());
assert!(validator.validate_env_value(&"x".repeat(501)).is_err());
}
#[test]
fn test_strict_validator() {
let validator = EnvSecurityValidator::strict();
let valid_128 = "A".repeat(128);
assert!(validator.validate_env_name_simple(&valid_128).is_ok());
let invalid_129 = "A".repeat(129);
assert!(validator.validate_env_name_simple(&invalid_129).is_err());
assert!(validator.validate_env_value(&"x".repeat(2048)).is_ok());
assert!(validator.validate_env_value(&"x".repeat(2049)).is_err());
}
#[test]
fn test_lenient_validator() {
let validator = EnvSecurityValidator::lenient();
let long_name = "A".repeat(500);
assert!(validator.validate_env_name_simple(&long_name).is_ok());
let long_value = "x".repeat(5000);
assert!(validator.validate_env_value(&long_value).is_ok());
assert!(validator.validate_env_name_simple("PATH").is_ok());
assert!(validator.validate_env_value("hello;world").is_ok());
}
#[test]
fn test_disabled_blocked_patterns() {
let config = EnvironmentValidationConfig::new().with_blocked_patterns_disabled();
let validator = EnvSecurityValidator::with_config(config);
assert!(validator.validate_env_name_simple("PATH").is_ok());
assert!(validator.validate_env_name_simple("HOME").is_ok());
assert!(validator.validate_env_name_simple("SECRET_KEY").is_ok());
}
#[test]
fn test_disabled_length_validation() {
let config = EnvironmentValidationConfig::new().with_length_validation_disabled();
let validator = EnvSecurityValidator::with_config(config);
let very_long_name = "A".repeat(1000);
assert!(validator.validate_env_name_simple(&very_long_name).is_ok());
let very_long_value = "x".repeat(10000);
assert!(validator.validate_env_value(&very_long_value).is_ok());
}
#[test]
fn test_disabled_encrypted_value_skip() {
let config = EnvironmentValidationConfig::new()
.with_length_validation_disabled()
.with_blocked_patterns_disabled()
.with_custom_blocked_patterns(vec![r".*SECRET.*".to_string()]);
let validator = EnvSecurityValidator::with_config(config);
let encrypted_value = "enc:ABC123XYZ789";
assert!(validator.validate_env_value(encrypted_value).is_ok());
let secret_with_encrypted = "MY_SECRET";
assert!(validator
.validate_env_name(secret_with_encrypted, Some(encrypted_value))
.is_ok());
}
#[test]
fn test_global_config_functions() {
let config = EnvironmentValidationConfig::new()
.with_max_name_length(512)
.with_max_value_length(8192);
let validator = EnvSecurityValidator::with_config(config.clone());
assert_eq!(config.max_name_length(), 512);
assert_eq!(config.max_value_length(), 8192);
let long_name = "A".repeat(512);
assert!(validator.validate_env_name_simple(&long_name).is_ok());
let invalid_513 = "A".repeat(513);
assert!(validator.validate_env_name_simple(&invalid_513).is_err());
}
#[test]
fn test_config_builder_pattern() {
let config = EnvironmentValidationConfig::new()
.with_max_name_length(64)
.with_max_value_length(1024)
.with_blocked_patterns_disabled()
.with_length_validation_disabled();
assert_eq!(config.max_name_length, 64);
assert_eq!(config.max_value_length, 1024);
assert!(!config.enable_blocked_patterns);
assert!(!config.enable_length_validation);
}
}
#[cfg(feature = "encryption")]
pub mod secure_string;
#[cfg(feature = "encryption")]
pub use secure_string::{
allocated_secure_strings, deallocated_secure_strings, SecureString, SecureStringBuilder,
SensitiveData, SensitivityLevel,
};
#[cfg(feature = "encryption")]
pub mod config_injector;
#[cfg(feature = "encryption")]
pub use config_injector::{
ConfigInjectionError, ConfigInjector, EnvironmentConfig, InjectionRecord,
};
#[cfg(feature = "encryption")]
pub mod input_validation;
#[cfg(feature = "encryption")]
pub use input_validation::{
ConfigValidationError as ConfigFieldValidationError, ConfigValidationResult, ConfigValidator,
ConfigValidatorBuilder, InputValidationError, InputValidator, SensitiveDataDetector,
SensitivityResult,
};
#[cfg(feature = "encryption")]
pub mod error_sanitization;
#[cfg(feature = "encryption")]
pub use error_sanitization::{
Error as SanitizationError, ErrorSanitizer, FilterResult, LogLevel, SafeResult, SecureLogger,
SensitiveDataFilter,
};
#[cfg(all(test, feature = "encryption"))]
mod security_tests;