use attackstr::expand_template;
use attackstr::{
apply_encoding, validate, BuiltinEncoding, Encoding, Grammar, GrammarMeta, IssueLevel,
MarkerPosition, PayloadConfig, PayloadDb, PayloadError, Technique, Variable,
};
use std::collections::HashMap;
mod test_depth_adversarial;
#[test]
fn grammar_explosion_100x100() {
let mut vars = HashMap::new();
let mut var_values = Vec::new();
for i in 0..100 {
var_values.push(Variable {
value: format!("v{i}"),
});
}
vars.insert("var".to_string(), var_values);
let mut techniques = Vec::new();
for i in 0..100 {
techniques.push(Technique {
name: format!("t{i}"),
template: "{var}".into(),
tags: vec![],
confidence: 1.0,
expected_pattern: None,
});
}
let grammar = Grammar {
meta: GrammarMeta {
name: "explosion".into(),
sink_category: "test".into(),
description: None,
tags: vec![],
severity: None,
cwe: None,
target_runtime: None,
},
contexts: vec![],
techniques,
encodings: vec![],
variables: vars,
};
let mut db = PayloadDb::with_config(PayloadConfig {
max_per_category: 10000,
deduplicate: false,
..PayloadConfig::default()
});
let toml_str = toml::to_string(&grammar).unwrap();
let result = db.load_toml(&toml_str);
assert!(
result.is_ok(),
"100x100 grammar should expand without errors"
);
let payloads = db.payloads("test");
assert_eq!(
payloads.len(),
10000,
"Expected 10,000 payloads (100 vars × 100 techniques)"
);
}
#[test]
fn self_referencing_variable_expansion() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "self-ref"
sink_category = "self-cat"
[[self_referencing]]
value = "{self_referencing}"
[[techniques]]
name = "t1"
template = "{self_referencing}"
"#,
);
assert!(
result.is_err(),
"Self-referencing variable should cause an error"
);
}
#[test]
fn extreme_template_length_many_placeholders() {
let mut db = PayloadDb::new();
let mut toml = String::from(
r#"
[grammar]
name = "many-vars"
sink_category = "many-cat"
"#,
);
for i in 0..1000 {
use std::fmt::Write;
let _ = write!(&mut toml, "\n[[v{i}]]\nvalue = \"x{i}\"");
}
toml.push_str("\n\n[[techniques]]\nname = \"t1\"\ntemplate = \"");
for i in 0..1000 {
use std::fmt::Write;
let _ = write!(&mut toml, "{{v{i}}}");
}
toml.push('"');
let result = db.load_toml(&toml);
assert!(
result.is_err(),
"should exceed depth limit and return an error"
);
}
#[test]
fn triple_url_encoding_chain() {
let input = "<script>alert(1)</script>";
let once = apply_encoding(input, "url_encode").unwrap();
let twice = apply_encoding(&once, "url_encode").unwrap();
let thrice = apply_encoding(&twice, "url_encode").unwrap();
assert!(
thrice.len() > twice.len(),
"Triple encoding should expand further"
);
assert!(
thrice.contains("%253C") || thrice.contains("%25"),
"Triple encoding should have nested percent escapes"
);
}
#[test]
fn variable_named_grammar_reserved() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "reserved-test"
sink_category = "reserved-cat"
[[grammars]]
value = "confusing_var"
[[techniques]]
name = "t1"
template = "{grammar}"
"#,
);
assert!(result.is_ok(), "reserved-word variable should still load");
let payloads = db.payloads("reserved-cat");
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].text, "confusing_var");
}
#[test]
fn payload_contains_marker_already() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "marker-test"
sink_category = "marker-cat"
[[techniques]]
name = "t1"
template = "SLN_MARKER_42_already_here"
"#,
);
let marked = db.payloads_with_marker("marker-cat", "SLN_MARKER_42");
assert_eq!(marked.len(), 1);
let text = &marked[0].text;
println!("Payload with existing marker: {text}");
assert!(text.contains("SLN_MARKER_42"));
}
#[test]
fn duplicate_technique_names() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "dup-test"
sink_category = "dup-cat"
[[techniques]]
name = "same_name"
template = "payload_a"
[[techniques]]
name = "same_name"
template = "payload_b"
"#,
);
assert!(
result.is_ok(),
"duplicate technique names should still load"
);
let payloads = db.payloads("dup-cat");
assert_eq!(
payloads.len(),
2,
"Duplicate technique names should both produce payloads"
);
}
#[test]
fn empty_variable_placeholder() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "empty-var"
sink_category = "empty-cat"
[[techniques]]
name = "t1"
template = "test{}end"
"#,
);
assert!(
result.is_ok(),
"empty placeholder template should load as literal"
);
let payloads = db.payloads("empty-cat");
assert_eq!(payloads[0].text, "test{}end");
}
#[test]
fn unicode_variable_names() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "unicode-var"
sink_category = "unicode-cat"
[["日本語"]]
value = "japanese"
[[techniques]]
name = "t1"
template = "{日本語}"
"#,
);
assert!(result.is_ok(), "unicode variable names should load");
let payloads = db.payloads("unicode-cat");
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].text, "japanese");
}
#[test]
fn null_bytes_in_payload_content() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "null-test"
sink_category = "null-cat"
[[techniques]]
name = "t1"
template = "hello\x00world"
"#,
);
assert!(
result.is_err(),
"null byte escape should be rejected by TOML parsing"
);
}
#[test]
fn extremely_long_variable_value() {
let mut db = PayloadDb::new();
let long_value = "A".repeat(100_000);
let toml = format!(
r#"
[grammar]
name = "long-var"
sink_category = "long-cat"
[[data]]
value = "{long_value}"
[[techniques]]
name = "t1"
template = "{{data}}"
"#
);
let result = db.load_toml(&toml);
assert!(result.is_ok(), "Should handle 100KB variable value");
let payloads = db.payloads("long-cat");
assert_eq!(payloads[0].text.len(), 100_000);
}
#[test]
fn duplicate_context_names() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "dup-ctx"
sink_category = "dup-ctx-cat"
[[contexts]]
name = "same_ctx"
prefix = "A"
suffix = ""
[[contexts]]
name = "same_ctx"
prefix = "B"
suffix = ""
[[techniques]]
name = "t1"
template = "{prefix}X{suffix}"
"#,
);
assert!(result.is_ok(), "duplicate contexts should still load");
let payloads = db.payloads("dup-ctx-cat");
let has_a = payloads.iter().any(|p| p.text.starts_with('A'));
let has_b = payloads.iter().any(|p| p.text.starts_with('B'));
assert!(
has_a && has_b,
"Both duplicate contexts should produce payloads"
);
}
#[test]
fn special_regex_in_expected_pattern() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "regex-test"
sink_category = "regex-cat"
[[techniques]]
name = "t1"
template = "test"
expected_pattern = "[a-z]+.*\\d{3}$"
"#,
);
let payloads = db.payloads("regex-cat");
assert_eq!(payloads.len(), 1);
assert_eq!(
payloads[0].expected_pattern,
Some("[a-z]+.*\\d{3}$".to_string())
);
}
#[test]
fn marker_with_special_characters() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "special-marker"
sink_category = "special-cat"
[[techniques]]
name = "t1"
template = "alert(1)"
"#,
);
let special_markers = vec![
"<script>",
"' OR 1=1 --",
"${jndi:ldap://evil}",
"{{{{ self }}}}", ];
for marker in special_markers {
let marked = db.payloads_with_marker("special-cat", marker);
assert_eq!(marked.len(), 1);
assert!(
marked[0].text.contains(marker),
"Marker '{marker}' should appear in payload"
);
}
}
#[test]
fn invalid_confidence_values() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "bad-confidence"
sink_category = "conf-cat"
[[techniques]]
name = "t1"
template = "test"
confidence = 999.9
[[techniques]]
name = "t2"
template = "test2"
confidence = -5.0
"#,
);
assert!(
result.is_err(),
"Loading should fail due to invalid confidence"
);
}
#[test]
fn template_literal_text_no_variables() {
let mut db = PayloadDb::new();
let toml = r#"
[grammar]
name = "literal"
sink_category = "literal-cat"
[[techniques]]
name = "t1"
template = "plain text no variables"
"#;
let result = db.load_toml(toml);
assert!(
result.is_ok(),
"plain text template should load: {result:?}"
);
let payloads = db.payloads("literal-cat");
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].text, "plain text no variables");
}
#[test]
fn template_empty_braces_does_not_panic() {
let mut db = PayloadDb::new();
let toml = r#"
[grammar]
name = "edge"
sink_category = "edge-cat"
[[techniques]]
name = "t1"
template = "{}"
"#;
let _result = db.load_toml(toml);
}
#[test]
fn completely_empty_grammar() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "empty"
sink_category = "empty-cat"
"#,
);
assert!(
result.is_ok(),
"empty grammar should load but produce no payloads"
);
let payloads = db.payloads("empty-cat");
assert!(payloads.is_empty());
}
#[test]
fn all_encodings_cascade() {
let input = "test";
let encodings = vec![
"url_encode",
"double_url",
"hex",
"unicode",
"html_entities",
"base64",
"octal",
"js_charcode",
];
let mut results = Vec::new();
let mut current = input.to_string();
for enc in &encodings {
current = apply_encoding(¤t, enc).unwrap();
results.push(current.clone());
}
for (i, result) in results.iter().enumerate() {
println!(
"After {} encodings: len={}, sample={}",
i + 1,
result.len(),
&result[..result.len().min(50)]
);
}
assert_ne!(results.last().unwrap(), input);
}
#[test]
fn variables_with_empty_values() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "empty-vars"
sink_category = "empty-var-cat"
[[vars]]
value = ""
[[vars]]
value = ""
[[techniques]]
name = "t1"
template = "X{var}Y"
"#,
);
let payloads = db.payloads("empty-var-cat");
assert!(payloads.iter().all(|p| p.text == "XY"));
}
#[test]
fn category_with_special_characters() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "special-cat"
sink_category = "path/to/category"
[[techniques]]
name = "t1"
template = "test"
"#,
);
assert!(result.is_ok(), "special category names should load");
let payloads = db.payloads("path/to/category");
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].text, "test");
}
#[test]
fn template_with_whitespace_escape_sequences() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "whitespace"
sink_category = "ws-cat"
[[techniques]]
name = "t1"
template = "line1\nline2\ttabbed"
"#,
);
let payloads = db.payloads("ws-cat");
assert_eq!(payloads.len(), 1);
assert!(
payloads[0].text.contains('\n'),
"Newline should be preserved"
);
assert!(payloads[0].text.contains('\t'), "Tab should be preserved");
}
#[test]
fn variable_name_looks_like_toml_key() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "toml-like"
sink_category = "toml-cat"
[[a.b.c]]
value = "nested_lookalike"
[[techniques]]
name = "t1"
template = "{a.b.c}"
"#,
);
assert!(
result.is_ok() || result.is_err(),
"Dotted variables should parse or fail cleanly"
);
if result.is_ok() {
let payloads = db.payloads("toml-cat");
assert!(
!payloads.is_empty(),
"If it succeeds, it should produce payloads"
);
}
}
#[test]
fn multiple_encodings_same_payload() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "multi-enc"
sink_category = "enc-cat"
[[techniques]]
name = "t1"
template = "test"
[[encodings]]
name = "raw"
transform = "identity"
[[encodings]]
name = "url"
transform = "url_encode"
[[encodings]]
name = "double"
transform = "double_url"
[[encodings]]
name = "hex"
transform = "hex"
"#,
);
let payloads = db.payloads("enc-cat");
assert!(
payloads.len() >= 2,
"at least 2 unique encodings: {}",
payloads.len()
);
let unique: std::collections::HashSet<_> = payloads.iter().map(|p| &p.text).collect();
assert!(unique.len() >= 2, "at least 2 unique: {}", unique.len());
}
#[test]
fn marker_replace_placeholder_not_found() {
let mut db = PayloadDb::with_config(PayloadConfig {
marker_position: MarkerPosition::Replace("{NOT_HERE}".into()),
..PayloadConfig::default()
});
let _ = db.load_toml(
r#"
[grammar]
name = "no-placeholder"
sink_category = "np-cat"
[[techniques]]
name = "t1"
template = "alert(1)"
"#,
);
let marked = db.payloads_with_marker("np-cat", "SLN_123");
assert_eq!(marked[0].text, "alert(1)");
}
#[test]
fn deeply_nested_brace_literals() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "nested-braces"
sink_category = "brace-cat"
[[techniques]]
name = "t1"
template = "{{{{{{{{literal_braces}}}}}}}}"
"#,
);
assert!(result.is_ok(), "nested brace literals should load");
let payloads = db.payloads("brace-cat");
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].text, "{{{{literal_braces}}}}");
}
#[test]
fn variable_value_with_braces() {
let mut lookup = HashMap::new();
lookup.insert("var".to_string(), vec!["{nested}".to_string()]);
let result = expand_template("{var}".into(), &lookup);
assert!(
result.is_ok(),
"template expansion with brace-valued variable should succeed"
);
let expanded = result.unwrap();
assert_eq!(expanded.len(), 1);
}
#[test]
fn rapid_config_changes() {
let mut db = PayloadDb::new();
for i in 0..100 {
let toml = format!(
r#"
[grammar]
name = "rapid{i}"
sink_category = "rapid-cat"
[[techniques]]
name = "t{i}"
template = "payload{i}"
"#
);
db.load_toml(&toml).unwrap();
}
let payloads = db.payloads("rapid-cat");
assert_eq!(payloads.len(), 100);
}
#[test]
fn validation_massive_issues() {
let mut techniques = Vec::new();
for i in 0..100 {
techniques.push(Technique {
name: format!("t{i}"),
template: format!("{{undefined_var_{i}}}"),
tags: vec![],
confidence: 2.0, expected_pattern: None,
});
}
let grammar = Grammar {
meta: GrammarMeta {
name: String::new(), sink_category: String::new(), description: None,
tags: vec![],
severity: None,
cwe: None,
target_runtime: None,
},
contexts: vec![],
techniques,
encodings: vec![Encoding {
name: "bad".into(),
transform: "unknown_transform".into(),
}],
variables: HashMap::new(),
};
let issues = validate(&grammar);
let errors = issues
.iter()
.filter(|i| i.level == IssueLevel::Error)
.count();
let warnings = issues
.iter()
.filter(|i| i.level == IssueLevel::Warning)
.count();
println!("Validation found {errors} errors and {warnings} warnings");
assert!(issues.len() > 100, "Should have many validation issues");
}
#[test]
fn unknown_encoding_transform() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "unknown-enc"
sink_category = "unknown-enc-cat"
[[techniques]]
name = "t1"
template = "test"
[[encodings]]
name = "mystery"
transform = "this_does_not_exist"
"#,
);
let err = result.expect_err("unknown encoding transform must fail closed at load time");
match err {
PayloadError::GrammarValidation { issues, .. } => {
assert!(
issues.iter().any(|i| i.message.contains("this_does_not_exist")
&& i.message.contains("unknown transform")),
"validation error must name the unknown transform, got: {issues:?}"
);
}
other => panic!("expected GrammarValidation error, got {other:?}"),
}
assert!(
db.payloads("unknown-enc-cat").is_empty(),
"a grammar rejected at load time must not contribute any payloads"
);
db.register_encoding("this_does_not_exist", |s| format!("X{s}X"));
db.load_toml(
r#"
[grammar]
name = "known-custom-enc"
sink_category = "custom-enc-cat"
[[techniques]]
name = "t1"
template = "test"
[[encodings]]
name = "mystery"
transform = "this_does_not_exist"
"#,
)
.expect("a registered custom encoding must satisfy load-time validation");
let payloads = db.payloads("custom-enc-cat");
assert!(
payloads.iter().any(|p| p.text == "XtestX" && p.encoding == "mystery"),
"custom transform must be applied, got: {payloads:?}"
);
}
#[test]
fn variable_named_prefix_or_suffix() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "prefix-var"
sink_category = "prefix-cat"
[[prefix]]
value = "OVERRIDE_PREFIX"
[[contexts]]
name = "ctx"
prefix = "REAL_PREFIX"
suffix = ""
[[techniques]]
name = "t1"
template = "{prefix}value"
"#,
);
let payloads = db.payloads("prefix-cat");
println!("Prefix resolution: {}", payloads[0].text);
assert!(
payloads[0].text.contains("REAL_PREFIX") || payloads[0].text.contains("OVERRIDE_PREFIX")
);
}
#[test]
fn very_long_technique_name() {
let long_name = "t".repeat(10000);
let toml = format!(
r#"
[grammar]
name = "long-name"
sink_category = "long-name-cat"
[[techniques]]
name = "{long_name}"
template = "test"
"#
);
let mut db = PayloadDb::new();
let result = db.load_toml(&toml);
assert!(result.is_ok(), "very long technique names should load");
let payloads = db.payloads("long-name-cat");
assert_eq!(payloads[0].technique.len(), 10000);
}
#[test]
fn circular_variable_references() {
let mut db = PayloadDb::new();
let result = db.load_toml(
r#"
[grammar]
name = "circular"
sink_category = "circular-cat"
[[a]]
value = "{b}"
[[b]]
value = "{a}"
[[techniques]]
name = "t1"
template = "{a}"
"#,
);
assert!(result.is_err(), "Circular variable references should fail");
}
#[test]
fn all_encodings_on_empty_string() {
for enc in BuiltinEncoding::ALL {
let result = apply_encoding("", enc).unwrap();
let expected = match *enc {
"null_byte" => "%00",
"charcode" | "js_charcode" => "String.fromCodePoint()",
"python_chr" => "\"\".join([])",
"sql_char" => "CONCAT()",
_ => "",
};
assert_eq!(
result, expected,
"unexpected empty-input encoding output for {enc}"
);
}
}
#[test]
fn duplicate_variable_names() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "dup-var"
sink_category = "dup-var-cat"
[[vars]]
value = "first"
[[vars]]
value = "second"
[[techniques]]
name = "t1"
template = "{var}"
"#,
);
let payloads = db.payloads("dup-var-cat");
assert_eq!(payloads.len(), 2);
let texts: Vec<_> = payloads.iter().map(|p| &p.text).collect();
assert!(texts.contains(&&"first".to_string()));
assert!(texts.contains(&&"second".to_string()));
}
#[test]
fn template_only_special_placeholders() {
let mut db = PayloadDb::new();
let _ = db.load_toml(
r#"
[grammar]
name = "special-only"
sink_category = "special-cat"
[[contexts]]
name = "ctx"
prefix = "PREFIX"
suffix = "SUFFIX"
[[techniques]]
name = "t1"
template = "{prefix}{suffix}"
"#,
);
let payloads = db.payloads("special-cat");
assert_eq!(payloads[0].text, "PREFIXSUFFIX");
}
#[test]
fn exponential_growth_memory_limit() {
let mut db = PayloadDb::new();
let toml = r#"
[grammar]
name = "exponential-growth"
sink_category = "exp-cat"
[[techniques]]
name = "t1"
template = "{a}"
[[a]]
value = "{b}{b}{b}{b}{b}{b}{b}{b}{b}{b}"
[[b]]
value = "{c}{c}{c}{c}{c}{c}{c}{c}{c}{c}"
[[c]]
value = "{d}{d}{d}{d}{d}{d}{d}{d}{d}{d}"
[[d]]
value = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"#;
let mut extremely_long_template = String::new();
for _ in 0..300_000 {
extremely_long_template.push_str("X");
}
let toml_mod = toml.replace(
"template = \"{a}\"",
&format!("template = \"{}\"", extremely_long_template),
);
let result = db.load_toml(&toml_mod);
assert!(result.is_err(), "Length over memory limit should fail");
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("prevent exponential variable growth")
|| err_str.contains("exceeding the limit"),
"Error message should mention length bounds: {err_str}"
);
}