use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;
static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(r"(?:^|[^0-9])%(?:\d{1,9}\$)?(?:hh|h|ll|l|L|z|j|t|q)?n\b").unwrap(),
Regex::new(r"%\d{6,}[diouxXeEfgGaAcspn]").unwrap(),
Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[xXp]){3,}").unwrap(),
Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[xXp][.\-_:]?){4,}").unwrap(),
Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?s[.\-_:]{0,2}){4,}").unwrap(),
Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[sxXp][.\-_:]{0,2}){6,}").unwrap(),
]
});
pub struct FormatStringDetector;
impl Detector for FormatStringDetector {
fn name(&self) -> &'static str {
"format_string"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
&PATTERNS,
self.name(),
AttackCategory::Injection,
Severity::Medium,
"Format string injection detected",
input,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{assert_clean, assert_detected};
fn det() -> FormatStringDetector {
FormatStringDetector
}
fn assert_hit(input: &str) {
assert_detected(&det(), input, AttackCategory::Injection, Severity::Medium);
}
#[test]
fn name_is_format_string() {
assert_eq!(det().name(), "format_string");
}
#[test]
fn detects_percent_n_variants() {
for input in [
"%n",
"AAAA%n",
"%1$n",
"%hn",
"%lln",
"%08x%08x%08x%n",
"%p %p %n",
] {
assert_hit(input);
}
}
#[test]
fn detects_width_bomb() {
for input in ["%99999999d", "%1000000s", "AAAA%2147483647d"] {
assert_hit(input);
}
}
#[test]
fn detects_repeated_leak_specifiers() {
for input in [
"%x%x%x%x",
"%p%p%p",
"%08x.%08x.%08x.%08x",
"%s%s%s%s",
"%x%p%s%x%p%s",
] {
assert_hit(input);
}
}
#[test]
fn ignores_benign_inputs() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"100% safe",
"50% off",
"discount 20%",
"%s 是占位符",
"%d%%",
"printf(\"%s\\n\", name)",
"a % b",
"下载进度 88%",
"http://example.com/?q=%20name",
"100%name",
"url: a%2Fb%3Fc",
] {
assert_clean(&det(), input);
}
}
#[test]
fn ignores_normal_printf_and_sql_templates() {
for input in [
"printf(\"%s, %s, %s, %s\\n\", a, b, c, d)",
"INSERT INTO t VALUES (%s, %s, %s, %s)",
"score=%s; name=%s; tag=%s; note=%s",
"user=%s ip=%s",
] {
assert_clean(&det(), input);
}
}
#[test]
fn ignores_short_leak_sequences() {
for input in ["%x %x %x", "%08x.%08x.%08x"] {
assert_clean(&det(), input);
}
assert_hit("%x%x%x");
}
#[test]
fn ignores_percent_sign_after_digits() {
for input in ["100%n", "50%n/a"] {
assert_clean(&det(), input);
}
assert_hit("AAAA%n");
}
#[test]
fn edge_cases() {
assert_clean(&det(), "");
assert_clean(&det(), " ");
assert_clean(&det(), "%");
assert_clean(&det(), "%%");
assert_clean(&det(), "%s");
assert_clean(&det(), "%s%s%s");
assert_clean(&det(), "%x%x");
assert_clean(&det(), "%p%p");
}
}