use std::panic::AssertUnwindSafe;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use sup_xml::{parse_bytes, ParseOptions};
const TIMEOUT_SECS: u64 = 10;
fn read_attack(name: &str) -> &'static [u8] {
macro_rules! f {
($n:literal) => {
if name == $n {
return include_bytes!(concat!(
"../../../tests/assets/xml/attacks/",
$n
));
}
};
}
f!("attribute_name_collision_hash.xml");
f!("billion_laughs.xml");
f!("billion_laughs_deep.xml");
f!("billion_laughs_utf8.xml");
f!("bom_only.xml");
f!("cdata_in_attribute.xml");
f!("charref_above_unicode.xml");
f!("charref_control.xml");
f!("charref_huge_number.xml");
f!("charref_nul.xml");
f!("charref_surrogate.xml");
f!("comment_double_hyphen.xml");
f!("deep_mixed_content.xml");
f!("deep_nesting_100k.xml");
f!("deep_nesting_10k.xml");
f!("deep_nesting_1k.xml");
f!("default_attr_namespace_interaction.xml");
f!("dtd_attlist_huge.xml");
f!("dtd_circular_includes.xml");
f!("dtd_external_only.xml");
f!("dtd_notation_redefine.xml");
f!("dtd_with_pe_in_internal_subset.xml");
f!("duplicate_attributes.xml");
f!("empty_document.xml");
f!("entity_expansion_in_attribute.xml");
f!("entity_only_predefined.xml");
f!("entity_undefined.xml");
f!("huge_attribute_value.xml");
f!("invalid_utf8_overlong.xml");
f!("invalid_utf8_surrogate.xml");
f!("long_attribute_name.xml");
f!("long_comment.xml");
f!("long_element_name.xml");
f!("long_pi_target.xml");
f!("long_text_node.xml");
f!("many_attributes.xml");
f!("mismatched_tags.xml");
f!("mixed_encoding.xml");
f!("multiple_roots.xml");
f!("namespace_prefix_explosion.xml");
f!("namespace_redefinition.xml");
f!("nested_comment.xml");
f!("nested_general_entity_cycle.xml");
f!("null_byte.xml");
f!("parameter_entity_recursion.xml");
f!("parameter_entity_self.xml");
f!("pi_xml_target.xml");
f!("prolog_after_root.xml");
f!("quadratic_blowup.xml");
f!("trailing_garbage.xml");
f!("unclosed_root.xml");
f!("unterminated_cdata.xml");
f!("unterminated_comment.xml");
f!("unterminated_pi.xml");
f!("unterminated_tag.xml");
f!("utf16_no_bom.xml");
f!("utf16_surrogate_lone.xml");
f!("utf7_encoding.xml");
f!("utf8_bom_wrong_decl.xml");
f!("whitespace_only.xml");
f!("xinclude_file.xml");
f!("xinclude_http.xml");
f!("xinclude_recursive.xml");
f!("xinclude_xpointer_bomb.xml");
f!("xml10_with_nel.xml");
f!("xml11_c0_controls.xml");
f!("xml_base_redefinition.xml");
f!("xml_space_preserve_nested.xml");
f!("xmlns_empty_uri.xml");
f!("xxe_expect.xml");
f!("xxe_file_read.xml");
f!("xxe_file_read_param.xml");
f!("xxe_http_ssrf.xml");
f!("xxe_netdoc.xml");
f!("xxe_oob_dtd.xml");
f!("xxe_php_filter.xml");
panic!("unknown attack fixture: {name} (add an entry in read_attack)");
}
#[derive(Debug)]
enum Outcome {
Ok,
Err(String),
Panicked,
TimedOut,
}
fn parse_guarded(bytes: &'static [u8], opts: ParseOptions) -> Outcome {
if cfg!(miri) {
return match std::panic::catch_unwind(AssertUnwindSafe(|| {
parse_bytes(bytes, &opts).map(|_doc| ()).map_err(|e| e.to_string())
})) {
Ok(Ok(())) => Outcome::Ok,
Ok(Err(msg)) => Outcome::Err(msg),
Err(_) => Outcome::Panicked,
};
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
parse_bytes(bytes, &opts).map(|_doc| ()).map_err(|e| e.to_string())
}));
let _ = tx.send(res);
});
match rx.recv_timeout(Duration::from_secs(TIMEOUT_SECS)) {
Ok(Ok(Ok(()))) => Outcome::Ok,
Ok(Ok(Err(msg))) => Outcome::Err(msg),
Ok(Err(_panic)) => Outcome::Panicked,
Err(_) => Outcome::TimedOut,
}
}
fn assert_rejected(name: &str) {
let bytes = read_attack(name);
let outcome = parse_guarded(bytes, ParseOptions::default());
match outcome {
Outcome::Err(_) => {}
Outcome::Ok => panic!("{name}: expected parse error, got success"),
Outcome::Panicked => panic!("{name}: parser PANICKED — must return Err, not panic"),
Outcome::TimedOut => {
panic!("{name}: parser HUNG past {TIMEOUT_SECS}s — possible DoS regression")
}
}
}
fn assert_handled_safely(name: &str) {
let bytes = read_attack(name);
let outcome = parse_guarded(bytes, ParseOptions::default());
match outcome {
Outcome::Ok | Outcome::Err(_) => {}
Outcome::Panicked => panic!("{name}: parser PANICKED — must return Err, not panic"),
Outcome::TimedOut => {
panic!("{name}: parser HUNG past {TIMEOUT_SECS}s — possible DoS regression")
}
}
}
fn assert_accepted(name: &str) {
let bytes = read_attack(name);
let outcome = parse_guarded(bytes, ParseOptions::default());
match outcome {
Outcome::Ok => {}
Outcome::Err(msg) => panic!("{name}: expected success, got error: {msg}"),
Outcome::Panicked => panic!("{name}: parser PANICKED"),
Outcome::TimedOut => {
panic!("{name}: parser HUNG past {TIMEOUT_SECS}s — possible DoS regression")
}
}
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn billion_laughs() {
assert_rejected("billion_laughs.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn billion_laughs_deep() {
assert_rejected("billion_laughs_deep.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn billion_laughs_utf8() {
assert_rejected("billion_laughs_utf8.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn quadratic_blowup() {
assert_rejected("quadratic_blowup.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn entity_expansion_in_attribute() {
assert_rejected("entity_expansion_in_attribute.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn parameter_entity_recursion() {
assert_rejected("parameter_entity_recursion.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn parameter_entity_self() {
assert_rejected("parameter_entity_self.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn nested_general_entity_cycle() {
assert_rejected("nested_general_entity_cycle.xml");
}
#[test]
fn xxe_file_read() {
assert_rejected("xxe_file_read.xml");
}
#[test]
fn xxe_file_read_param() {
assert_rejected("xxe_file_read_param.xml");
}
#[test]
fn xxe_http_ssrf() {
assert_rejected("xxe_http_ssrf.xml");
}
#[test]
fn xxe_oob_dtd() {
assert_handled_safely("xxe_oob_dtd.xml");
}
#[test]
fn xxe_php_filter() {
assert_rejected("xxe_php_filter.xml");
}
#[test]
fn xxe_expect() {
assert_rejected("xxe_expect.xml");
}
#[test]
fn xxe_netdoc() {
assert_rejected("xxe_netdoc.xml");
}
#[test]
fn xinclude_file() {
assert_handled_safely("xinclude_file.xml");
}
#[test]
fn xinclude_recursive() {
assert_handled_safely("xinclude_recursive.xml");
}
#[test]
fn xinclude_http() {
assert_handled_safely("xinclude_http.xml");
}
#[test]
fn xinclude_xpointer_bomb() {
assert_handled_safely("xinclude_xpointer_bomb.xml");
}
#[test]
fn deep_nesting_1k() {
assert_rejected("deep_nesting_1k.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn deep_nesting_10k() {
assert_rejected("deep_nesting_10k.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn deep_nesting_100k() {
assert_rejected("deep_nesting_100k.xml");
}
#[test]
fn deep_mixed_content() {
assert_rejected("deep_mixed_content.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn many_attributes() {
assert_handled_safely("many_attributes.xml");
}
#[test]
fn duplicate_attributes() {
assert_rejected("duplicate_attributes.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn huge_attribute_value() {
assert_handled_safely("huge_attribute_value.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn attribute_name_collision_hash() {
assert_handled_safely("attribute_name_collision_hash.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn namespace_prefix_explosion() {
assert_handled_safely("namespace_prefix_explosion.xml");
}
#[test]
fn namespace_redefinition() {
assert_handled_safely("namespace_redefinition.xml");
}
#[test]
fn xmlns_empty_uri() {
assert_handled_safely("xmlns_empty_uri.xml");
}
#[test]
fn unterminated_comment() {
assert_rejected("unterminated_comment.xml");
}
#[test]
fn unterminated_cdata() {
assert_rejected("unterminated_cdata.xml");
}
#[test]
fn unterminated_pi() {
assert_rejected("unterminated_pi.xml");
}
#[test]
fn unterminated_tag() {
assert_rejected("unterminated_tag.xml");
}
#[test]
fn nested_comment() {
assert_rejected("nested_comment.xml");
}
#[test]
fn comment_double_hyphen() {
assert_rejected("comment_double_hyphen.xml");
}
#[test]
fn cdata_in_attribute() {
assert_rejected("cdata_in_attribute.xml");
}
#[test]
fn pi_xml_target() {
assert_handled_safely("pi_xml_target.xml");
}
#[test]
fn bom_only() {
assert_rejected("bom_only.xml");
}
#[test]
fn empty_document() {
assert_rejected("empty_document.xml");
}
#[test]
fn whitespace_only() {
assert_rejected("whitespace_only.xml");
}
#[test]
fn trailing_garbage() {
assert_rejected("trailing_garbage.xml");
}
#[test]
fn multiple_roots() {
assert_rejected("multiple_roots.xml");
}
#[test]
fn prolog_after_root() {
assert_rejected("prolog_after_root.xml");
}
#[test]
fn mismatched_tags() {
assert_rejected("mismatched_tags.xml");
}
#[test]
fn unclosed_root() {
assert_rejected("unclosed_root.xml");
}
#[test]
fn utf16_no_bom() {
assert_handled_safely("utf16_no_bom.xml");
}
#[test]
fn utf8_bom_wrong_decl() {
assert_handled_safely("utf8_bom_wrong_decl.xml");
}
#[test]
fn utf7_encoding() {
assert_handled_safely("utf7_encoding.xml");
}
#[test]
fn mixed_encoding() {
assert_rejected("mixed_encoding.xml");
}
#[test]
fn invalid_utf8_overlong() {
assert_rejected("invalid_utf8_overlong.xml");
}
#[test]
fn invalid_utf8_surrogate() {
assert_rejected("invalid_utf8_surrogate.xml");
}
#[test]
fn null_byte() {
assert_rejected("null_byte.xml");
}
#[test]
fn utf16_surrogate_lone() {
assert_handled_safely("utf16_surrogate_lone.xml");
}
#[test]
fn charref_above_unicode() {
assert_rejected("charref_above_unicode.xml");
}
#[test]
fn charref_surrogate() {
assert_rejected("charref_surrogate.xml");
}
#[test]
fn charref_nul() {
assert_rejected("charref_nul.xml");
}
#[test]
fn charref_control() {
assert_rejected("charref_control.xml");
}
#[test]
fn charref_huge_number() {
assert_rejected("charref_huge_number.xml");
}
#[test]
fn entity_undefined() {
assert_rejected("entity_undefined.xml");
}
#[test]
fn entity_only_predefined() {
assert_accepted("entity_only_predefined.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn long_element_name() {
assert_handled_safely("long_element_name.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn long_attribute_name() {
assert_handled_safely("long_attribute_name.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn long_pi_target() {
assert_handled_safely("long_pi_target.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn long_text_node() {
assert_handled_safely("long_text_node.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn long_comment() {
assert_handled_safely("long_comment.xml");
}
#[test]
fn dtd_external_only() {
assert_handled_safely("dtd_external_only.xml");
}
#[test]
fn dtd_circular_includes() {
assert_handled_safely("dtd_circular_includes.xml");
}
#[test]
#[cfg_attr(miri, ignore = "Miri is too slow, TODO re-enable for Miri later")]
fn dtd_attlist_huge() {
assert_handled_safely("dtd_attlist_huge.xml");
}
#[test]
fn dtd_notation_redefine() {
assert_handled_safely("dtd_notation_redefine.xml");
}
#[test]
fn dtd_with_pe_in_internal_subset() {
assert_handled_safely("dtd_with_pe_in_internal_subset.xml");
}
#[test]
fn xml11_c0_controls() {
assert_handled_safely("xml11_c0_controls.xml");
}
#[test]
fn xml10_with_nel() {
assert_handled_safely("xml10_with_nel.xml");
}
#[test]
fn xml_space_preserve_nested() {
assert_accepted("xml_space_preserve_nested.xml");
}
#[test]
fn xml_base_redefinition() {
assert_accepted("xml_base_redefinition.xml");
}
#[test]
fn default_attr_namespace_interaction() {
assert_handled_safely("default_attr_namespace_interaction.xml");
}