1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
13 vec![
14 Regex::new(r"(?:^|[^0-9])%(?:\d{1,9}\$)?(?:hh|h|ll|l|L|z|j|t|q)?n\b").unwrap(),
18 Regex::new(r"%\d{6,}[diouxXeEfgGaAcspn]").unwrap(),
20 Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[xXp]){3,}").unwrap(),
22 Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[xXp][.\-_:]?){4,}").unwrap(),
25 Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?s[.\-_:]{0,2}){4,}").unwrap(),
27 Regex::new(r"(?:%[0-9]{0,4}(?:hh|h|ll|l|L|z|j|t|q)?[sxXp][.\-_:]{0,2}){6,}").unwrap(),
29 ]
30});
31
32pub struct FormatStringDetector;
33
34impl Detector for FormatStringDetector {
35 fn name(&self) -> &'static str {
36 "format_string"
37 }
38
39 fn detect(&self, input: &str) -> Option<DetectionResult> {
40 regex_detect(
41 &PATTERNS,
42 self.name(),
43 AttackCategory::Injection,
44 Severity::Medium,
45 "Format string injection detected",
46 input,
47 )
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::test_helpers::{assert_clean, assert_detected};
55
56 fn det() -> FormatStringDetector {
57 FormatStringDetector
58 }
59
60 fn assert_hit(input: &str) {
61 assert_detected(&det(), input, AttackCategory::Injection, Severity::Medium);
62 }
63
64 #[test]
65 fn name_is_format_string() {
66 assert_eq!(det().name(), "format_string");
67 }
68
69 #[test]
70 fn detects_percent_n_variants() {
71 for input in [
72 "%n",
73 "AAAA%n",
74 "%1$n",
75 "%hn",
76 "%lln",
77 "%08x%08x%08x%n",
78 "%p %p %n",
79 ] {
80 assert_hit(input);
81 }
82 }
83
84 #[test]
85 fn detects_width_bomb() {
86 for input in ["%99999999d", "%1000000s", "AAAA%2147483647d"] {
87 assert_hit(input);
88 }
89 }
90
91 #[test]
92 fn detects_repeated_leak_specifiers() {
93 for input in [
94 "%x%x%x%x",
95 "%p%p%p",
96 "%08x.%08x.%08x.%08x",
97 "%s%s%s%s",
98 "%x%p%s%x%p%s",
99 ] {
100 assert_hit(input);
101 }
102 }
103
104 #[test]
105 fn ignores_benign_inputs() {
106 for input in [
107 "Hello, this is a normal text input. Nothing suspicious here.",
108 "100% safe",
109 "50% off",
110 "discount 20%",
111 "%s 是占位符",
112 "%d%%",
113 "printf(\"%s\\n\", name)",
114 "a % b",
115 "下载进度 88%",
116 "http://example.com/?q=%20name",
117 "100%name",
118 "url: a%2Fb%3Fc",
119 ] {
120 assert_clean(&det(), input);
121 }
122 }
123
124 #[test]
125 fn ignores_normal_printf_and_sql_templates() {
126 for input in [
129 "printf(\"%s, %s, %s, %s\\n\", a, b, c, d)",
130 "INSERT INTO t VALUES (%s, %s, %s, %s)",
131 "score=%s; name=%s; tag=%s; note=%s",
132 "user=%s ip=%s",
133 ] {
134 assert_clean(&det(), input);
135 }
136 }
137
138 #[test]
139 fn ignores_short_leak_sequences() {
140 for input in ["%x %x %x", "%08x.%08x.%08x"] {
143 assert_clean(&det(), input);
144 }
145 assert_hit("%x%x%x");
147 }
148
149 #[test]
150 fn ignores_percent_sign_after_digits() {
151 for input in ["100%n", "50%n/a"] {
153 assert_clean(&det(), input);
154 }
155 assert_hit("AAAA%n");
156 }
157
158 #[test]
159 fn edge_cases() {
160 assert_clean(&det(), "");
161 assert_clean(&det(), " ");
162 assert_clean(&det(), "%");
163 assert_clean(&det(), "%%");
164 assert_clean(&det(), "%s");
165 assert_clean(&det(), "%s%s%s");
166 assert_clean(&det(), "%x%x");
168 assert_clean(&det(), "%p%p");
169 }
170}