From 66cd116d54325e71dfbdd51e04bf167f7ec6353e Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Fri, 12 Jun 2026 14:10:28 +0000 Subject: [PATCH] feat(validation): tighten ACME contact validation to require non-empty email with @ sign --- src/config/validation.rs | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/config/validation.rs b/src/config/validation.rs index ea1c84d..6de8cf7 100644 --- a/src/config/validation.rs +++ b/src/config/validation.rs @@ -150,6 +150,13 @@ pub fn validate( errors.push(ValidationError::AcmeContactInvalid { bind_addr: listener.bind_addr.clone(), }); + } else { + let email = &contact[7..]; + if email.is_empty() || !email.contains('@') { + errors.push(ValidationError::AcmeContactInvalid { + bind_addr: listener.bind_addr.clone(), + }); + } } } "manual" => { @@ -1009,6 +1016,58 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn rule19_acme_contact_mailto_empty_email() { + let mut config = valid_static_config(); + config.listeners[0].tls = TlsConfig { + mode: "acme".to_string(), + acme_domains: vec!["test.local".to_string()], + acme_cache_dir: "/tmp/cache".to_string(), + acme_directory: "production".to_string(), + acme_contact: "mailto:".to_string(), + cert_path: String::new(), + key_path: String::new(), + }; + config.listeners[0].sites = vec![SiteConfig { + host: "test.local".to_string(), + upstream: "127.0.0.1:8080".to_string(), + ..valid_dynamic_config().sites[0].clone() + }]; + let dynamic = valid_dynamic_config(); + let result = validate(&config, &dynamic, false); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert!(errors + .iter() + .any(|e| matches!(e, ValidationError::AcmeContactInvalid { .. }))); + } + + #[test] + fn rule19_acme_contact_mailto_no_at_sign() { + let mut config = valid_static_config(); + config.listeners[0].tls = TlsConfig { + mode: "acme".to_string(), + acme_domains: vec!["test.local".to_string()], + acme_cache_dir: "/tmp/cache".to_string(), + acme_directory: "production".to_string(), + acme_contact: "mailto:user".to_string(), + cert_path: String::new(), + key_path: String::new(), + }; + config.listeners[0].sites = vec![SiteConfig { + host: "test.local".to_string(), + upstream: "127.0.0.1:8080".to_string(), + ..valid_dynamic_config().sites[0].clone() + }]; + let dynamic = valid_dynamic_config(); + let result = validate(&config, &dynamic, false); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert!(errors + .iter() + .any(|e| matches!(e, ValidationError::AcmeContactInvalid { .. }))); + } + #[test] fn valid_config_passes() { let dir = tempfile::tempdir().unwrap();