Skip to main content

ecat_graphql/
validation.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2//! 执行前查询预算校验:防查询放大 DoS。
3//! 解析器已限制 MAX_DEPTH=32 并拒绝别名/多顶层字段,此处补节点总数预算。
4
5use crate::parser::{ParsedField, SelectionSet};
6
7pub const DEFAULT_MAX_COMPLEXITY: usize = 10_000;
8
9#[derive(Debug, Clone, Copy)]
10pub struct QueryLimits {
11    /// 选择树节点总数上限(含根字段),超过即拒绝。
12    pub max_complexity: usize,
13}
14
15impl Default for QueryLimits {
16    fn default() -> Self {
17        Self {
18            max_complexity: DEFAULT_MAX_COMPLEXITY,
19        }
20    }
21}
22
23/// 解析通过后、任何执行工作开始前调用。超过预算返回错误字符串
24/// (与 parse 错误同一形状,由 execute 打包为 GraphQL errors)。
25pub fn validate(field: &ParsedField, limits: &QueryLimits) -> Result<(), String> {
26    let n = complexity(&field.selection) + 1;
27    if n > limits.max_complexity {
28        Err(format!(
29            "query complexity {n} exceeds limit {}",
30            limits.max_complexity
31        ))
32    } else {
33        Ok(())
34    }
35}
36
37fn complexity(sel: &Option<SelectionSet>) -> usize {
38    match sel {
39        None => 0,
40        Some(set) => set.values().map(|n| 1 + complexity(&n.selection)).sum(),
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use crate::parser::parse_query;
48    use crate::{GraphQLSchema, execute};
49    use serde_json::Value;
50
51    fn parse(q: &str) -> ParsedField {
52        parse_query(q, &Value::Null).unwrap()
53    }
54
55    fn limits(max: usize) -> QueryLimits {
56        QueryLimits {
57            max_complexity: max,
58        }
59    }
60
61    #[test]
62    fn over_budget_query_is_rejected() {
63        // 根 a + b + c + x + d + e = 6 个节点,预算 5
64        let f = parse("{ a { b { c { x } } d { e } } }");
65        let err = validate(&f, &limits(5)).unwrap_err();
66        assert!(err.contains("complexity 6 exceeds limit 5"), "got: {err}");
67    }
68
69    #[test]
70    fn under_budget_query_passes() {
71        let f = parse("{ a { b { c } d { e } } }");
72        assert!(validate(&f, &limits(5)).is_ok());
73        assert!(validate(&f, &QueryLimits::default()).is_ok());
74    }
75
76    #[test]
77    fn bare_field_costs_one() {
78        let f = parse("{ ping }");
79        assert!(validate(&f, &limits(1)).is_ok());
80        assert!(validate(&f, &limits(0)).is_err());
81    }
82
83    #[test]
84    fn execute_rejects_over_budget_before_resolving() {
85        let schema = GraphQLSchema::new()
86            .with_limits(limits(1))
87            .query_fn("boom", |_v| {
88                Box::pin(async { Ok(serde_json::json!("never")) })
89            });
90        let rt = tokio::runtime::Runtime::new().unwrap();
91        let err = rt
92            .block_on(execute(&schema, "{ boom { x } }", &Value::Null))
93            .unwrap_err();
94        assert!(
95            err.iter().any(|e| e.contains("exceeds limit")),
96            "got: {err:?}"
97        );
98    }
99
100    #[test]
101    fn deeply_nested_query_is_rejected_at_parse() {
102        let deep = "{ a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a { a } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }";
103        let err = parse_query(deep, &Value::Null).unwrap_err();
104        assert!(err.contains("selection too deep"), "got: {err}");
105    }
106
107    #[test]
108    fn alias_flood_is_rejected_at_parse() {
109        // 解析器不支持别名:别名在预算校验前即被拒绝,无放大路径
110        let err = parse_query("{ a: b c: d e: f g: h i: j k: l m: n }", &Value::Null).unwrap_err();
111        assert!(err.contains("aliases not supported"), "got: {err}");
112    }
113}