← 中文资源

简体中文版本

GraphQL — 深层嵌套查询拖垮服务器

保护 GraphQL 服务器免受深度与复杂度炸弹,读解析器计时找到指数扇出,并强制深度、复杂度与速率限制。

阅读英文原文

症状

  乘法在解析器轨迹里可见:user 解析 1 次、其 friends 解析 500 次、再一层 25 万次、第三层 1.25 亿次——friends-of-friends 字段每层 500 倍扇出,一个请求就要一亿多次解析,同一批数据走 REST 列表端点毫秒返回,因为形状由服务器控制。租户隔离的破绽同样显眼:一条精心构造的查询让整台服务器对所有租户无响应。CPU 被单个请求钉死数分钟、resolver 同字段解析百万次,是这类炸弹的量化指纹。

诊断思路

  先读解析器轨迹按层统计执行次数,乘法即暴露;再采样统计入站查询的深度分布——正常客户端 5 层以内,20 层以上的尾部要么是构造炸弹要么是生成客户端的意外递归;最后用手工炸弹测试验证:未设防的服务器会执行它(漏洞坐实),有深度限制的在执行前拒绝。

根因

  GraphQL 执行客户端发送的任何结构。没有深度与复杂度限制时,列表字段上的嵌套查询让解析器工作量指数级放大——每层扇出成逐条解析,一个请求的需求就能超过全部正常流量的总和。

修复与验证

  验证期设防:depthLimit(7) 拒绝更深文档、成本分析封顶 query cost;逐条扇出用 DataLoader 批量化——每层一条查询而不是每项一条;生产客户端关闭任意文本通道,只允许持久化查询(注册文档、发送哈希);按复杂度加权的令牌级限速。验证时构造炸弹在验证期被拒并给出清晰错误、代表性合法查询通过、负载测试下每请求的解析器执行计数保持有界。深度限制压到合法客户端需要之下会打断 API——先测量真实查询形状再选阈值。

代码示例

Query.user            1 execution
  User.friends        500 executions
    User.friends      250,000 executions
      User.friends    125,000,000 executions   <- the bomb
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7),                       // reject deeper documents
    createCostAnalysis({ maximumCost: 1000 }),
  ],
});
const userLoader = new DataLoader((ids) =>
  db.users.findByIds(ids)               // one query per level, not per item
);
// persisted queries only: clients register documents, send hashes
if (!isPersistedQuery(request)) {
  return reject('ad-hoc queries disabled');
}