Skip to main content

fraiseql_core/graphql/
complexity.rs

1// GraphQL query complexity analysis to prevent DoS attacks
2// Limits: max depth, max field count, max total complexity score
3
4/// Query complexity configuration
5#[derive(Debug, Clone)]
6pub struct ComplexityConfig {
7    /// Maximum query depth (nesting level) - default: 15
8    pub max_depth:  usize,
9    /// Maximum field count in a single query - default: 100
10    pub max_fields: usize,
11    /// Maximum complexity score (depth * field_count) - default: 500
12    pub max_score:  usize,
13}
14
15impl Default for ComplexityConfig {
16    fn default() -> Self {
17        Self {
18            max_depth:  15,
19            max_fields: 100,
20            max_score:  500,
21        }
22    }
23}
24
25/// Query complexity analyzer
26pub struct ComplexityAnalyzer {
27    config: ComplexityConfig,
28}
29
30impl ComplexityAnalyzer {
31    /// Create a new analyzer with default config
32    #[must_use]
33    pub fn new() -> Self {
34        Self {
35            config: ComplexityConfig::default(),
36        }
37    }
38
39    /// Create with custom config
40    #[must_use]
41    pub fn with_config(config: ComplexityConfig) -> Self {
42        Self { config }
43    }
44
45    /// Analyze query complexity
46    /// Returns (max_depth, field_count, total_score)
47    #[must_use]
48    pub fn analyze_complexity(&self, query: &str) -> (usize, usize, usize) {
49        // Parse query string to count nesting and fields
50        let mut max_depth = 0;
51        let mut current_depth = 0;
52        let mut field_count = 0;
53        let mut in_braces = false;
54
55        for ch in query.chars() {
56            match ch {
57                '{' => {
58                    in_braces = true;
59                    current_depth += 1;
60                    max_depth = max_depth.max(current_depth);
61                },
62                '}' => {
63                    if current_depth > 0 {
64                        current_depth -= 1;
65                    }
66                    in_braces = false;
67                },
68                '(' | ')' => {
69                    // Argument delimiters - not counted as fields
70                },
71                c if in_braces && c.is_alphabetic() => {
72                    // Count this as a potential field start
73                    field_count += 1;
74                },
75                _ => {},
76            }
77        }
78
79        let total_score = max_depth * field_count.max(1);
80        (max_depth, field_count, total_score)
81    }
82
83    /// Check if query exceeds limits
84    pub fn is_query_too_complex(&self, query: &str) -> Result<(), String> {
85        let (depth, fields, score) = self.analyze_complexity(query);
86
87        if depth > self.config.max_depth {
88            return Err(format!("Query depth {} exceeds maximum {}", depth, self.config.max_depth));
89        }
90
91        if fields > self.config.max_fields {
92            return Err(format!(
93                "Query field count {} exceeds maximum {}",
94                fields, self.config.max_fields
95            ));
96        }
97
98        if score > self.config.max_score {
99            return Err(format!(
100                "Query complexity score {} exceeds maximum {}",
101                score, self.config.max_score
102            ));
103        }
104
105        Ok(())
106    }
107}
108
109impl Default for ComplexityAnalyzer {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_simple_query_complexity() {
121        let analyzer = ComplexityAnalyzer::new();
122        let query = "{ users { id name } }";
123        let (depth, _fields, _score) = analyzer.analyze_complexity(query);
124        assert!(depth <= 3);
125    }
126
127    #[test]
128    fn test_deeply_nested_query() {
129        let analyzer = ComplexityAnalyzer::new();
130        let query = "{ a { b { c { d { e { f { g { h } } } } } } } }";
131        let (depth, _fields, _score) = analyzer.analyze_complexity(query);
132        assert!(depth >= 8);
133    }
134
135    #[test]
136    fn test_query_too_deep() {
137        let config = ComplexityConfig {
138            max_depth:  5,
139            max_fields: 100,
140            max_score:  500,
141        };
142        let analyzer = ComplexityAnalyzer::with_config(config);
143
144        let query = "{ a { b { c { d { e { f { g { h } } } } } } } }";
145        assert!(analyzer.is_query_too_complex(query).is_err());
146    }
147
148    #[test]
149    fn test_query_within_limits() {
150        let analyzer = ComplexityAnalyzer::new();
151        let query = "{ users { id name email } posts { id title } }";
152        assert!(analyzer.is_query_too_complex(query).is_ok());
153    }
154
155    #[test]
156    fn test_complexity_score() {
157        let analyzer = ComplexityAnalyzer::new();
158        let query = "{ users { id name email } }";
159        let (_depth, _fields, score) = analyzer.analyze_complexity(query);
160        // Score should be reasonable
161        assert!(score <= 500);
162    }
163}