安全
保护 GraphQL API 免受恶意操作的侵害
与任何类型的 API 一样,您需要考虑应使用哪些安全措施来保护 GraphQL 实现的服务器资源和底层数据源在请求执行期间的安全。
许多通用的 API 安全最佳实践适用于 GraphQL,尤其是在与给定实现所选的传输协议相关联时。然而,也有特定于 GraphQL 的安全注意事项可以使 API 层更能抵御潜在的攻击。
在本文中,我们将调查 GraphQL 的潜在攻击向量——其中许多是拒绝服务攻击——以及分层安全策略如何帮助保护 GraphQL API 免受恶意操作的侵害。
传输层安全
GraphQL 规范不要求请求使用特定的传输协议,但对于无状态的查询和变更操作,HTTP 是一个流行的选择。对于生命周期较长的订阅操作,通常使用 WebSockets 或服务器发送事件。无论选择哪种协议,传输层的安全注意事项都可以为 GraphQL API 提供重要的第一道防线。
用于任何类型 API 的相同安全措施,只要它们使用给定的传输协议提供服务,通常也应适用于 GraphQL。例如,当使用 HTTP 进行查询和变更时,您应该使用 HTTPS 来加密数据,为请求设置适当的超时持续时间,并且,如果使用 HTTP 缓存,请确保敏感数据被私密缓存(或根本不缓存)。
需求控制
受信任的文档
对于仅为第一方客户端服务的 GraphQL API,您可以按需编写开发环境中的操作,然后锁定这些操作,以便在生产环境中只能由前端应用发送它们。具体来说,使用受信任的文档将允许您创建可针对 Schema 执行的操作的允许列表。
在开发过程中,客户端可以通过构建步骤将它们的 GraphQL 文档提交到服务器的允许列表中,其中每个文档都存储在一个唯一的 ID(通常是其哈希值)下。在运行时,客户端可以发送文档 ID 而不是完整的 GraphQL 文档,服务器将只执行已知文档 ID 的请求。
受信任的文档只是被认为无恶意的持久化文档,通常是因为它们是由您的开发人员编写并通过代码审查批准的。目前正在努力将持久化文档作为GraphQL-over-HTTP 规范的一部分进行标准化——越来越多的 GraphQL 客户端和服务器已经支持它们(有时沿用其旧的误称:持久化查询)。
请记住,受信任的文档不能用于公共 API,因为第三方客户端发送的操作是事先未知的。然而,接下来的其他建议可以帮助保护公共 GraphQL API 免受恶意操作的侵害。
分页字段
为 GraphQL API 实施需求控制的第一步是限制可以从单个字段查询的数据量。当一个字段输出一个 List 类型并且结果列表可能返回大量数据时,应该对其进行分页,以限制单次请求中返回的项目最大数量。
例如,在此操作中,比较 friends 字段的无界输出与 friendsConnection 字段返回的有限数据
在分页页面上阅读更多关于用于分页字段的基于连接的模型。
深度限制
GraphQL 的优势之一是客户端可以编写反映 API 中暴露的数据之间关系的表达性操作。然而,当选择集中的字段深度嵌套时,某些查询可能会变得循环。
query {
hero {
name
friends {
name
friends {
name
friends {
name
friends {
name
}
}
}
}
}
}即使通过对底层数据源进行批量请求来解决 N+1 问题,过度嵌套的字段仍可能对服务器资源造成过度负载并影响 API 性能。
因此,最好限制单个操作可以具有的字段的最大深度。许多 GraphQL 实现都提供配置选项,允许您指定 GraphQL 文档的最大深度,如果请求在执行开始前超过此限制,则向客户端返回错误。
由于嵌套列表字段会导致返回数据量呈指数级增长,建议对列表可以嵌套的深度应用一个单独的较小限制。
对于客户端可能有合法用例进行深度嵌套查询,并且对所有查询设置较低的全面限制不切实际的情况,您可能需要依赖诸如速率限制或查询复杂度分析等技术。
广度与批量限制
除了限制操作深度之外,还应设置护栏来限制单个操作中包含的顶级字段和字段别名的数量。
考虑执行以下查询操作时会发生什么
query {
viewer {
friends1: friends(limit: 1) { name }
friends2: friends(limit: 2) { name }
friends3: friends(limit: 3) { name }
# ...
friends100: friends(limit: 100) { name }
}
}即使此查询的整体深度很浅,API 的底层数据源仍将不得不处理大量的请求来解析别名 hero 字段的数据。
类似地,客户端可以在一个请求中发送包含许多批量操作的 GraphQL 文档
query NewHopeHero {
hero(episode: NEWHOPE) {
name
}
}
query EmpireHero {
hero(episode: EMPIRE) {
name
}
}
### ...
query JediHero {
hero(episode: JEDI) {
name
}
}根据客户端实现,查询批量处理可能是限制往返服务器次数以获取渲染用户界面所需所有数据的有用策略。但是,对单个批次中允许的总查询数应设置一个上限。
与深度限制一样,GraphQL 实现可能具有配置选项来限制操作的广度、字段别名的使用和批量处理。
速率限制
深度、广度和批量限制有助于防止循环查询和批量攻击等广泛类别的恶意操作,但它们不能提供一种声明某个特定字段在解析时计算成本高昂的方法。因此,对于更高级的需求控制要求,您可能希望实施速率限制。
速率限制可以在应用程序的不同层中进行,例如在网络层或业务逻辑层。由于 GraphQL 允许客户端精确指定它们在查询中需要的数据,因此服务器可能无法预先知道请求是否包含在执行期间会对资源造成更高负载的字段。因此,为 GraphQL API 应用有用的速率限制通常需要与简单地在网络层中跟踪一段时间内的总传入请求数不同的方法;因此,通常建议在业务逻辑层内部应用速率限制。
查询复杂度分析
通过为 Schema 中的类型和字段分配权重,您可以使用查询复杂度分析这一技术来估算传入请求的成本。如果请求中包含的字段组合超过每个请求的最大允许成本,您可以选择直接拒绝该请求。估计的成本也可以纳入速率限制:如果请求继续,请求的总成本可以从分配给特定时间段内客户端的总查询预算中扣除。
虽然 GraphQL 规范没有提供关于实施查询复杂度分析或 API 速率限制的指南,但有一个社区维护的草案规范用于实现支持这些计算的自定义类型系统指令。
Schema 考量
验证和清理参数值
GraphQL 是强类型的,请求的验证阶段将根据类型系统检查解析后的文档以确定其有效性。但是,在某些情况下,您可能会发现内置的标量类型不足以确保客户端提供的参数值适合安全地执行字段解析器。
考虑与 createReview 变更一起使用的以下 ReviewInput 类型
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}取决于哪个客户端应用程序将提交和显示评论数据,可能有必要清理为 commentary 字段提供的字符串,以防用户提交不安全的 HTML。
清理步骤应发生在 createReview 解析器函数执行期间调用的业务逻辑层中。为了将其暴露给前端开发人员,它也可以作为 Schema 的一部分,使用自定义的标量类型来表示。
内省
内省是 GraphQL 的强大功能,它允许您查询 GraphQL API 以获取有关其 Schema 的信息。然而,仅为第一方客户端服务的 GraphQL API 可能会在非开发环境中禁止内省查询。
请注意,禁用内省可以用作“混淆安全”的一种形式,但它本身不足以完全隐藏 GraphQL API。它可以作为更广泛安全策略的一部分,用于限制潜在攻击者发现 API 信息的能力。为了对敏感的 Schema 详细信息和用户数据提供更全面的保护,应使用受信任的文档和授权。
错误消息
在响应页面上,我们看到 GraphQL 实现可能会向开发人员提供有关请求中验证错误的有用信息,以及有关如何解决这些错误的建议。例如
这些提示在调试客户端错误时可能很有帮助,但它们可能会在生产环境中泄露过多关于 Schema 的信息,超出了我们希望透露的范围。在开发环境之外的 GraphQL 响应中隐藏详细的错误信息很重要,因为即使禁用了内省,攻击者也可能通过运行带有不正确字段名称的众多操作来推断整个 Schema 的结构。
除了请求错误之外,字段执行期间引发的错误的详细信息也应被屏蔽,因为它们可能会泄露有关服务器或底层数据源的敏感信息。
身份验证和授权
关于 GraphQL API 的身份验证相关注意事项已在授权页面上深入讨论。
总结
总结保护 GraphQL API 的这些建议
- 针对所选传输协议的通用安全注意事项通常也适用于 GraphQL API
- 根据您的要求,可以在 GraphQL 中通过多种方式实现需求控制,包括受信任的文档、列表字段的分页、深度限制、广度/批量限制以及速率限制
- GraphQL Schema 可以帮助支持对客户端提供的值进行验证和清理
- 禁用内省和隐藏错误信息可以使 API Schema 难以被发现,但它们单独使用通常是不够的(文档允许列表可能是更有效的解决方案)
联合 (Federation)
学习 GraphQL 联邦如何通过将服务组合成统一的 Schema 来实现模块化、可扩展的 API。