Skip to main content

security_rust/injection/
graphql_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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        // near misses: not quite the introspection keyword, or not deep enough
88        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}