GraphQL的灵活性是一个双刃剑——客户端可以请求任意深度的嵌套数据。一个恶意构造的查询可以在一个请求中触发上千次数据库查询,消耗大量服务器资源。防御这种攻击的第一步是量化查询的"成本"。
查询成本分析的基本思路是:为每个字段分配一个成本值,查询的总成本是所有字段成本之和。标量字段成本为1,列表字段成本为1×列表大小,嵌套对象的成本递归计算。
import { createComplexityLimitRule } from 'graphql-validation-complexity'; const complexityLimit = createComplexityLimitRule(500, { onCost: (cost) => console.log(`查询成本: ${cost}`), onComplete: (cost) => console.log(`查询完成, 最终成本: ${cost}`), }); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [complexityLimit], });
使用自定义成本:
type Query { users(limit: Int = 10): [User!]! @cost(complexity: 5, multipliers: ["limit"]) user(id: ID!): User! @cost(complexity: 1) search(query: String!, limit: Int = 10): [SearchResult!]! @cost(complexity: 3, multipliers: ["limit"]) }
multipliers 参数让列表字段的成本随列表大小线性增长。users(limit: 100) 的成本是 5×100=500,users(limit: 10) 的成本是 5×10=50。
深度限制是最简单粗暴但有效的防护——直接限制查询的嵌套深度。
import { depthLimit } from 'graphql-depth-limit'; const server = new ApolloServer({ typeDefs, resolvers, validationRules: [depthLimit(7)], });
深度7意味着查询最多嵌套7层。一个典型的安全查询:
Query → users → posts → author → posts → author → posts 1 2 3 4 5 6 7
超过7层的查询在验证阶段就被拒绝,不会执行。建议设置5-7层的深度限制——太低会影响正常的业务查询,太高则防护效果弱。
除了深度和成本限制,还应该设置执行超时:
const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // 设置请求超时 req.setTimeout(5000); // 5秒 return createContext(req); }, });
超时控制防止慢查询长时间占用服务器资源。结合查询成本分析,可以建立一个多层防护体系:
除了深度、成本和超时,还需要考虑以下攻击向量:
字段枚举攻击:攻击者通过查询所有字段来获取完整的数据结构,包括可能包含敏感信息的字段。防御方法:字段级别的权限控制,确保每个字段都有适当的访问限制。
别名轰炸:攻击者用大量别名重复查询同一字段,绕过深度限制。防御方法:限制查询中的别名数量。
批量查询攻击:在单个HTTP请求中发送多个查询(通过GraphQL的多操作支持或简单地在查询文本中写多个操作)。防御方法:限制每个请求的操作数量。
信息泄露:错误消息中暴露内部实现细节(数据库名、SQL语句等)。防御方法:生产环境中屏蔽内部错误详情,只返回泛化的错误消息。
const server = new ApolloServer({ typeDefs, resolvers, formatError: (formattedError, error) => { // 内部错误不暴露细节 if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') { return { message: '服务器内部错误', code: 'INTERNAL_SERVER_ERROR', }; } return formattedError; }, // 限制请求大小 bodyParserConfig: { limit: '1mb' }, });
持久化查询(APQ)不仅优化性能,更是安全防护的利器。当服务器配置为"只接受已注册的查询"时,攻击者无法发送自定义查询——他们只能执行预先审核过的查询。
// Apollo Server APQ配置 const server = new ApolloServer({ typeDefs, resolvers, persistedQueries: { cache: new InMemoryLRUCache({ maxSize: 1000 // 缓存最多1000个查询hash }) }, // 可选:强制APQ模式(只接受已注册查询) // 未注册的查询直接返回 PersistedQueryNotFound });
APQ+查询成本分析+深度限制+超时控制,构成了一道完整的安全防护链。在实际部署中,这些措施应该组合使用——没有单一的银弹。
成本分析有两种流派:静态成本(按 Schema 字段声明固定值)和动态成本(结合参数计算)。静态成本简单稳定,但无法反映真实开销——一个 user 字段成本 1,一个需要跨服务调用的 orders 字段成本也是 1,显然不公平。动态成本用 multipliers 让成本随 limit、first 等参数线性甚至平方增长,更接近真实,但配置工作量大、容易误伤正常查询。
务实的做法是分层:用深度限制兜住最危险的嵌套爆炸,用静态成本卡住明显超重的查询,对已知的重字段单独做动态成本。不要一开始就追求精确的成本模型——先让攻击者无法轻易打垮服务,再逐步校准阈值。
查询成本分析防的是"单次查询太贵",限流防的是"查询次数太多"。两种限流维度:
// 概念示例:基于令牌桶的限流中间件 const rateLimiter = { key: (req) => req.user?.id ?? req.ip, limit: 100, windowMs: 60_000, onExceeded: () => { throw new GraphQLError('请求过于频繁', { extensions: { code: 'RATE_LIMITED' } }); }, };
一个典型的攻击链:攻击者对公开的 introspection 端点发起查询,拿到全部字段后,构造一个深度 12 层的嵌套查询,让每个层级都触发一次数据库查询,再配合别名把同一字段重复几百次。没有防护的服务可能在一个请求内被打满数据库连接池。防护后的表现是:深度限制在验证阶段拒绝,成本分析在进入执行前拒绝,限流挡住后续重试——三个措施各守一道关卡,任何一个单独存在都不够。
