email_sanitization.rs 101 lignes · 3337 octets
//! Email content sanitization utilities
//!
//! This module provides utilities for sanitizing email content to prevent XSS attacks
//! and ensure email content is safe for rendering.

/// Sanitize email content to prevent XSS attacks
///
/// This function escapes HTML special characters in user-provided content.
/// It's a safety measure in addition to template escaping (Askama escape = "html").
///
/// # Arguments
///
/// * `content` - Content to sanitize
///
/// # Returns
///
/// Sanitized content with HTML special characters escaped
///
/// # Examples
///
/// ```
/// use rustwarden_core::services::email_sanitization::sanitize_email_content;
///
/// let malicious = "<script>alert('XSS')</script>";
/// let safe = sanitize_email_content(malicious);
/// assert_eq!(safe, "&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;");
/// ```
pub fn sanitize_email_content(content: &str) -> String {
    content
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#x27;")
}

/// Mask an email address for log output (e.g. `jo***@ex***`).
///
/// Keeps at most the first two characters of the local part and of the
/// domain so log lines, audit messages and hook metadata never contain a
/// full address (SIEM-friendly).
///
/// Shared helper: used by
/// [`EmailChangeService`](crate::services::email_change_service::EmailChangeService)
/// for its `audit_logs` rows and `on_audit_event` payloads.
pub(crate) fn mask_email(email: &str) -> String {
    match email.split_once('@') {
        Some((local, domain)) => {
            let local_visible: String = local.chars().take(2).collect();
            let domain_visible: String = domain.chars().take(2).collect();
            format!("{local_visible}***@{domain_visible}***")
        }
        None => "***".to_string(),
    }
}

/// Validate email address format
///
/// # Arguments
///
/// * `email` - Email address to validate
///
/// # Returns
///
/// `true` if email format is valid, `false` otherwise
/// SEC-L7: Uses the `validator` crate for robust RFC-compliant email validation.
pub fn is_valid_email_format(email: &str) -> bool {
    validator::ValidateEmail::validate_email(&email.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sanitize_email_content() {
        // Test that function exists and works
        let content = "test content";
        let sanitized = sanitize_email_content(content);
        assert_eq!(sanitized, content);
    }

    #[test]
    fn test_is_valid_email_format() {
        assert!(is_valid_email_format("user@example.com"));
        assert!(is_valid_email_format("test.user@example.co.uk"));
        assert!(!is_valid_email_format("invalid-email"));
        assert!(!is_valid_email_format("user@"));
        assert!(!is_valid_email_format("@example.com"));
        assert!(!is_valid_email_format("user @example.com")); // Contains space
    }

    #[test]
    fn mask_email_never_leaks_full_address() {
        assert_eq!(mask_email("john.doe@example.com"), "jo***@ex***");
        assert_eq!(mask_email("a@b.c"), "a***@b.***");
        assert_eq!(mask_email("no-at-sign"), "***");
        // Multi-byte local part must not panic.
        assert_eq!(mask_email("émile@exemple.fr"), "ém***@ex***");
    }
}