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