security_rust/injection/
graphql_injection.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r"(?i)__schema").unwrap(),
10 Regex::new(r"(?i)__type\s*\{").unwrap(),
11 Regex::new(r"(?i)__typename").unwrap(),
12 Regex::new(r"\{[^{}]*\{[^{}]*\{[^{}]*\{[^{}]*\{").unwrap(),
13 ]
14});
15
16pub struct GraphQlInjectionDetector;
17
18impl Detector for GraphQlInjectionDetector {
19 fn name(&self) -> &'static str {
20 "graphql_injection"
21 }
22
23 fn detect(&self, input: &str) -> Option<DetectionResult> {
24 regex_detect(
25 &PATTERNS,
26 self.name(),
27 AttackCategory::Injection,
28 Severity::Medium,
29 "GraphQL injection/introspection detected",
30 input,
31 )
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 fn det() -> GraphQlInjectionDetector {
40 GraphQlInjectionDetector
41 }
42
43 fn assert_hit(input: &str) {
44 crate::test_helpers::assert_detected(
45 &det(),
46 input,
47 AttackCategory::Injection,
48 Severity::Medium,
49 );
50 }
51
52 #[test]
53 fn name_is_graphql_injection() {
54 assert_eq!(det().name(), "graphql_injection");
55 }
56
57 #[test]
58 fn detects_common_payloads() {
59 for input in [
60 "{ __schema { types { name } } }",
61 "query { __type { name } }",
62 "query { __typename }",
63 "{a{b{c{d{e{f}}}}}}",
64 "fragment F on __Type { name }",
65 ] {
66 assert_hit(input);
67 }
68 }
69
70 #[test]
71 fn benign_inputs_not_detected() {
72 for input in [
73 "Hello, this is a normal text input. Nothing suspicious here.",
74 "query { user(id: 1) { name } }",
75 r#"{"a": {"b": {"c": {"d": 1}}}}"#,
76 "The schema was updated today",
77 ] {
78 assert!(det().detect(input).is_none(), "false positive: {input}");
79 }
80 }
81
82 #[test]
83 fn edge_cases() {
84 assert!(det().detect("").is_none());
85 assert!(det().detect(" \t\n ").is_none());
86 assert!(det().detect("你好世界 こんにちは").is_none());
87 assert!(det().detect("{__schem}").is_none());
89 assert!(det().detect("schema").is_none());
90 assert!(det().detect("{{{{").is_none());
91 }
92
93 #[test]
94 fn obfuscated_variants_detected() {
95 for input in ["{ __SCHEMA { types } }", "__TYPENAME", "__Type { name }"] {
96 assert_hit(input);
97 }
98 }
99}