Skip to main content

security_rust/injection/
graphql_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
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(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Medium, "GraphQL injection/introspection detected", input)
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    fn det() -> GraphQlInjectionDetector {
33        GraphQlInjectionDetector
34    }
35
36    fn assert_hit(input: &str) {
37        crate::test_helpers::assert_detected(
38            &det(),
39            input,
40            AttackCategory::Injection,
41            Severity::Medium,
42        );
43    }
44
45    #[test]
46    fn name_is_graphql_injection() {
47        assert_eq!(det().name(), "graphql_injection");
48    }
49
50    #[test]
51    fn detects_common_payloads() {
52        for input in [
53            "{ __schema { types { name } } }",
54            "query { __type { name } }",
55            "query { __typename }",
56            "{a{b{c{d{e{f}}}}}}",
57            "fragment F on __Type { name }",
58        ] {
59            assert_hit(input);
60        }
61    }
62
63    #[test]
64    fn benign_inputs_not_detected() {
65        for input in [
66            "Hello, this is a normal text input. Nothing suspicious here.",
67            "query { user(id: 1) { name } }",
68            r#"{"a": {"b": {"c": {"d": 1}}}}"#,
69            "The schema was updated today",
70        ] {
71            assert!(det().detect(input).is_none(), "false positive: {input}");
72        }
73    }
74
75    #[test]
76    fn edge_cases() {
77        assert!(det().detect("").is_none());
78        assert!(det().detect(" \t\n ").is_none());
79        assert!(det().detect("你好世界 こんにちは").is_none());
80        // near misses: not quite the introspection keyword, or not deep enough
81        assert!(det().detect("{__schem}").is_none());
82        assert!(det().detect("schema").is_none());
83        assert!(det().detect("{{{{").is_none());
84    }
85
86    #[test]
87    fn obfuscated_variants_detected() {
88        for input in ["{ __SCHEMA { types } }", "__TYPENAME", "__Type { name }"] {
89            assert_hit(input);
90        }
91    }
92}