use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
use regex::Regex;
use std::sync::LazyLock;
static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(r"(?i)__schema").unwrap(),
Regex::new(r"(?i)__type\s*\{").unwrap(),
Regex::new(r"(?i)__typename").unwrap(),
Regex::new(r"\{[^{}]*\{[^{}]*\{[^{}]*\{[^{}]*\{").unwrap(),
]
});
pub struct GraphQlInjectionDetector;
impl Detector for GraphQlInjectionDetector {
fn name(&self) -> &'static str {
"graphql_injection"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Medium, "GraphQL injection/introspection detected", input)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det() -> GraphQlInjectionDetector {
GraphQlInjectionDetector
}
fn assert_hit(input: &str) {
crate::test_helpers::assert_detected(
&det(),
input,
AttackCategory::Injection,
Severity::Medium,
);
}
#[test]
fn name_is_graphql_injection() {
assert_eq!(det().name(), "graphql_injection");
}
#[test]
fn detects_common_payloads() {
for input in [
"{ __schema { types { name } } }",
"query { __type { name } }",
"query { __typename }",
"{a{b{c{d{e{f}}}}}}",
"fragment F on __Type { name }",
] {
assert_hit(input);
}
}
#[test]
fn benign_inputs_not_detected() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"query { user(id: 1) { name } }",
r#"{"a": {"b": {"c": {"d": 1}}}}"#,
"The schema was updated today",
] {
assert!(det().detect(input).is_none(), "false positive: {input}");
}
}
#[test]
fn edge_cases() {
assert!(det().detect("").is_none());
assert!(det().detect(" \t\n ").is_none());
assert!(det().detect("你好世界 こんにちは").is_none());
assert!(det().detect("{__schem}").is_none());
assert!(det().detect("schema").is_none());
assert!(det().detect("{{{{").is_none());
}
#[test]
fn obfuscated_variants_detected() {
for input in ["{ __SCHEMA { types } }", "__TYPENAME", "__Type { name }"] {
assert_hit(input);
}
}
}