跳到内容

GraphQL 备忘录

简介

GraphQL 是一种由 Facebook 最初开发的开源查询语言,可用作 REST 和 SOAP 的替代方案来构建 API。自 2012 年问世以来,它因其为 API 构建者和调用者提供的原生灵活性而广受欢迎。有多种语言实现了 GraphQL 服务器和客户端。许多公司使用 GraphQL,包括 GitHub、Credit Karma、Intuit 和 PayPal。

本备忘录提供了在使用 GraphQL 时需要考虑的各个方面的指导

  • 对所有传入数据应用适当的输入验证检查。
  • 昂贵的查询将导致拒绝服务 (DoS),因此请添加检查以限制或阻止过于昂贵的查询。
  • 确保 API 具有适当的访问控制检查。
  • 禁用不安全的默认配置(例如,过多错误、内省、GraphiQL 等)。

常见攻击

最佳实践和建议

输入验证

添加严格的输入验证有助于防止注入和 DoS。GraphQL 的主要设计是用户提供一个或多个标识符,后端有许多数据获取器使用给定的标识符进行 HTTP、DB 或其他调用。这意味着用户输入将包含在 HTTP 请求、DB 查询或其他请求/调用中,这为可能导致各种注入攻击或 DoS 的注入提供了机会。

有关执行输入验证和防止注入的完整详细信息,请参阅 OWASP 备忘录中关于输入验证和通用注入防御的部分。

一般实践

验证所有传入数据,只允许有效值(即白名单)。

注入防御

处理打算传递给另一个解释器(例如 SQL/NoSQL/ORM、OS、LDAP、XML)的输入时

  • 始终选择提供安全 API 的库/模块/包,例如参数化语句。
    • 确保您遵循文档,以便正确使用工具
    • 使用 ORM 和 ODM 是一个不错的选择,但它们必须正确使用才能避免诸如ORM 注入之类的缺陷。
  • 如果此类工具不可用,请始终根据目标解释器的最佳实践对输入数据进行转义/编码
    • 选择一个文档齐全且积极维护的转义/编码库。许多语言和框架都内置了此功能。

有关更多信息,请参阅以下页面

流程验证

在使用用户输入时,即使经过清理和/或验证,也不应将其用于某些会使用户控制数据流的目的。例如,不要向用户提供的宿主发出 HTTP/资源请求(除非有绝对的业务需求)。

DoS 防御

DoS 是一种针对 API 可用性和稳定性的攻击,它可能使 API 变慢、无响应或完全不可用。本 CS 详细介绍了在应用程序层和技术堆栈的其他层限制 DoS 攻击可能性的几种方法。还有一份专门针对 DoS 主题的 CS。

以下是针对 GraphQL 限制 DoS 潜力的建议

  • 对传入查询添加深度限制
  • 对传入查询添加数量限制
  • 添加分页以限制单个响应中可以返回的数据量
  • 在应用程序层、基础设施层或两者都添加合理的超时
  • 考虑执行查询成本分析并强制执行每个查询的最大允许成本
  • 对每个 IP 或用户(或两者)的传入请求强制执行速率限制,以防止基本的 DoS 攻击
  • 在服务器端实现批处理和缓存技术(Facebook 的DataLoader 可用于此)

查询限制(深度 & 数量)

在 GraphQL 中,每个查询都有一个深度(例如嵌套对象),并且查询中请求的每个对象都可以指定一个数量(例如,一个对象的 99999999 个)。默认情况下,这两者都可以是无限的,这可能导致 DoS。您应该设置深度和数量限制以防止 DoS,但这通常需要少量自定义实现,因为 GraphQL 不原生支持。有关这些攻击以及如何添加深度和数量限制的更多信息,请参阅页面。添加分页也有助于提高性能。

使用 graphql-java 的 API 可以利用内置的 MaxQueryDepthInstrumentation 进行深度限制。使用 JavaScript 的 API 可以使用 graphql-depth-limit 实现深度限制,并使用 graphql-input-number 实现数量限制。

这是一个深度为 N 的 GraphQL 查询示例

query evil {            # Depth: 0
  album(id: 42) {       # Depth: 1
    songs {             # Depth: 2
      album {           # Depth: 3
        ...             # Depth: ...
        album {id: N}   # Depth: N
      }
    }
  }
}

这是一个请求 99999999 个对象的 GraphQL 查询示例

query {
  author(id: "abc") {
    posts(first: 99999999) {
      title
    }
  }
}

超时

添加超时是一种简单的方法,可以限制任何单个请求可以消耗的资源量。但超时并非总是有效的,因为它们可能直到恶意查询已经消耗了过多资源后才激活。超时要求因 API 和数据获取机制而异;没有一个超时值可以通用。

在应用程序级别,可以为查询和解析器函数添加超时。此选项通常更有效,因为一旦达到超时,查询/解析就可以停止。GraphQL 不原生支持查询超时,因此需要自定义代码。有关在 GraphQL 中使用超时的更多信息,请参阅此博客文章或以下两个示例。

JavaScript 超时示例

代码片段来自此 SO 回答

request.incrementResolverCount =  function () {
    var runTime = Date.now() - startTime;
    if (runTime > 10000) {  // a timeout of 10 seconds
      if (request.logTimeoutError) {
        logger('ERROR', `Request ${request.uuid} query execution timeout`);
      }
      request.logTimeoutError = false;
      throw 'Query execution has timeout. Field resolution aborted';
    }
    this.resolverCount++;
  };

使用Instrumentation的 Java 超时示例

public class TimeoutInstrumentation extends SimpleInstrumentation {
    @Override
    public DataFetcher<?> instrumentDataFetcher(
            DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters
    ) {
        return environment ->
            Observable.fromCallable(() -> dataFetcher.get(environment))
                .subscribeOn(Schedulers.computation())
                .timeout(10, TimeUnit.SECONDS)  // timeout of 10 seconds
                .blockingFirst();
    }
}

基础设施超时

另一种通常更容易的添加超时的方法是在 HTTP 服务器(Apache/httpdnginx)、反向代理或负载均衡器上添加超时。然而,基础设施超时通常不准确,并且比应用程序级别的超时更容易被绕过。

查询成本分析

查询成本分析涉及为传入查询中的字段或类型解析分配成本,以便服务器可以拒绝运行成本过高或将消耗过多资源的查询。这不容易实现,也可能并非总是必要的,但它是防止 DoS 最彻底的方法。有关实现此控制的更多详细信息,请参阅此博客文章中的“查询成本分析”部分。

Apollo 建议

在您花费大量时间实施查询成本分析之前,请确定您是否真的需要它。尝试用恶意查询使您的预演 API 崩溃或变慢,看看您能达到什么程度——也许您的 API 没有这些类型的嵌套关系,或者也许它可以完美地处理一次获取数千条记录,并且不需要查询成本分析!

使用 graphql-java 的 API 可以利用内置的 MaxQueryComplexityInstrumentationto 来强制执行最大查询复杂度。使用 JavaScript 的 API 可以利用 graphql-cost-analysisgraphql-validation-complexity 来强制执行最大查询成本。

速率限制

基于每个 IP 或用户(针对匿名和未经授权的访问)强制执行速率限制可以帮助限制单个用户向服务发送垃圾请求并影响性能的能力。理想情况下,这可以通过 WAF、API 网关或 Web 服务器(NginxApache/HTTPD)来完成,以减少添加速率限制的工作量。

或者,您可以进行一些复杂的节流并在您的代码中实现它(非简单)。有关 GraphQL 特定速率限制的更多信息,请参阅此处的“节流”部分。

服务器端批处理和缓存

为了提高 GraphQL API 的效率并减少其资源消耗,可以使用批处理和缓存技术来防止在短时间内对数据片段进行重复请求。Facebook 的 DataLoader 工具是实现此目的的一种方式。

系统资源管理

不正确限制 API 可使用的资源量(例如 CPU 或内存)可能会损害 API 的响应性和可用性,使其容易受到 DoS 攻击。有些限制可以在操作系统级别完成。

在 Linux 上,可以使用 控制组 (cgroups)用户限制 (ulimits)Linux 容器 (LXC) 的组合。

然而,容器化平台往往使这项任务变得容易得多。有关在使用容器时如何防止 DoS,请参阅Docker 安全备忘录中的资源限制部分。

访问控制

为确保 GraphQL API 具有适当的访问控制,请执行以下操作

  • 始终验证请求者是否有权查看或变更/修改他们请求的数据。这可以通过RBAC或其他访问控制机制来完成。
  • 在边和节点上强制执行授权检查(参阅错误报告示例,其中节点没有授权检查,但边有)。
  • 使用接口联合创建结构化、分层的数据类型,可根据请求者权限返回更多或更少对象属性。
  • 查询和变更解析器可用于执行访问控制验证,可能使用一些 RBAC 中间件。
  • 在任何生产或公共可访问环境中系统范围禁用内省查询
  • 在生产或公共可访问环境中禁用 GraphiQL 和其他类似的模式探索工具。

通用数据访问

GraphQL 请求通常包含一个或多个对象的直接 ID,以便获取或修改它们。例如,对某张图片的请求可能包含该图片在数据库中的主键 ID。与任何请求一样,服务器必须验证调用者是否有权访问他们请求的对象。但有时开发人员会错误地认为拥有对象的 ID 意味着调用者应该拥有访问权限。在这种情况下未能验证请求者访问权限被称为对象级别身份验证损坏,也称为IDOR

即使并非有意,GraphQL API 也可能支持使用其 ID 访问对象。有时查询对象中存在 `node` 或 `nodes` 或两者字段,这些字段可用于直接通过 `ID` 访问对象。您可以通过在命令行上运行此命令来检查您的模式是否具有这些字段(假设 `schema.json` 包含您的 GraphQL 模式):`cat schema.json | jq ".data.__schema.types[] | select(.name==\"Query\") | .fields[] | .name" | grep node`。从模式中删除这些字段应该会禁用该功能,但您应该始终应用适当的授权检查以验证调用者是否有权访问他们请求的对象。

查询访问(数据获取)

作为 GraphQL API 的一部分,将有各种可返回的数据字段。需要考虑的一点是,您是否希望对这些字段设置不同级别的访问权限。例如,您可能只希望某些消费者能够获取某些数据字段,而不是允许所有消费者都能检索所有可用字段。这可以通过在代码中添加检查来完成,以确保请求者应该能够读取他们正在尝试获取的字段。

变更访问(数据操作)

GraphQL 除了最常见的数据获取用例之外,还支持数据的变更或操作。如果 API 实现/允许变更,则可能需要设置访问控制,以限制哪些消费者(如果有)可以通过 API 修改数据。需要变更访问控制的设置将包括只打算请求者进行只读访问的 API,或只有某些方才能修改某些字段的 API。

批处理攻击

GraphQL 支持批处理请求,也称为查询批处理。这使得调用者可以在单个网络调用中批处理多个查询或批处理多个对象实例的请求,从而导致所谓的批处理攻击。这是一种针对 GraphQL 的暴力攻击形式,通常允许更快、更难检测的漏洞利用。以下是执行查询批处理的最常见方式

[
  {
    query: < query 0 >,
    variables: < variables for query 0 >,
  },
  {
    query: < query 1 >,
    variables: < variables for query 1 >,
  },
  {
    query: < query n >
    variables: < variables for query n >,
  }
]

以下是一个单个批处理 GraphQL 调用请求多个不同 droid 对象实例的查询示例

query {
  droid(id: "2000") {
    name
  }
  second:droid(id: "2001") {
    name
  }
  third:droid(id: "2002") {
    name
  }
}

在这种情况下,它可以用于在极少量的网络请求中枚举服务器上存储的每个可能的 droid 对象,而与标准 REST API 不同,后者需要请求者为每个不同的 droid ID 提交不同的网络请求。这种类型的攻击可能导致以下问题

  • 应用程序级 DoS 攻击 - 单个网络调用中的大量查询或对象请求可能导致数据库挂起或耗尽其他可用资源(例如内存、CPU、下游服务)。
  • 枚举服务器上的对象,例如用户、电子邮件和用户 ID。
  • 暴力破解密码、双因素认证代码 (OTP)、会话令牌或其他敏感值。
  • WAF、RASP、IDS/IPS、SIEM 或其他安全工具很可能无法检测到这些攻击,因为它们看起来只是一个单一请求,而不是大量的网络流量。
  • 这种攻击很可能绕过 Nginx 或其他代理/网关等工具中现有的速率限制,因为它们依赖于查看原始请求数量。

缓解批处理攻击

为了缓解此类攻击,您应该在代码级别对传入请求设置限制,以便它们可以按请求应用。主要有 3 种选择

  • 在代码中添加对象请求速率限制
  • 防止敏感对象的批处理
  • 限制一次可以运行的查询数量

一种选择是创建代码级别的速率限制,限制调用者可以请求的对象数量。这意味着后端将跟踪调用者请求了多少个不同的对象实例,以便即使他们在一个网络调用中批量请求对象,在请求过多对象后也会被阻止。这复制了 WAF 或其他工具将执行的网络级别速率限制。

另一个选项是阻止对您不希望被暴力破解的敏感对象进行批处理,例如用户名、电子邮件、密码、OTP、会话令牌等。这样,攻击者就被迫像攻击 REST API 一样攻击 API,并为每个对象实例进行不同的网络调用。这不受原生支持,因此需要自定义解决方案。但是,一旦此控制措施到位,其他标准控制措施将正常运行,以帮助防止任何暴力破解。

限制可以批处理和同时运行的操作数量是缓解导致 DoS 的 GraphQL 批处理攻击的另一种选择。但这并非万能药,应与其他方法结合使用。

安全配置

默认情况下,大多数 GraphQL 实现都有一些不安全的默认配置,应进行更改

  • 不要返回过多错误消息(例如,禁用堆栈跟踪和调试模式)。
  • 根据您的需求禁用或限制内省和 GraphiQL。
  • 如果内省被禁用,则会建议输入错误的字段

内省 + GraphiQL

GraphQL 通常默认启用内省和/或 GraphiQL,且无需身份验证。这使得您的 API 消费者可以了解您 API 的所有信息、模式、变更、已弃用字段,有时甚至是意外的“私有字段”。

如果您的 API 旨在供外部客户端使用,这可能是预期的配置,但如果 API 旨在仅供内部使用,这也可能是一个问题。尽管不建议使用“通过模糊化实现安全”,但考虑删除内省以避免任何泄露可能是一个好主意。如果您的 API 公开使用,您可能需要考虑为未认证或未经授权的用户禁用它。

对于内部 API,最简单的方法是系统范围禁用内省。请参阅此页面或查阅您的 GraphQL 实现文档以了解如何完全禁用内省。如果您的实现不原生支持禁用内省,或者如果您希望允许某些消费者/角色拥有此访问权限,您可以在服务中构建一个过滤器,只允许经过批准的消费者访问内省系统。

请记住,即使内省被禁用,攻击者仍然可以通过暴力破解来猜测字段。此外,GraphQL 有一个内置功能,当请求者提供的字段名与现有字段相似(但不正确)时,会返回提示(例如,请求包含 usr,响应会询问 您是想说“user”吗?)。如果已禁用内省,您应该考虑禁用此功能,以减少暴露,但并非所有 GraphQL 实现都支持这样做。Shapeshifter 是一个能够做到这一点的工具。

禁用内省 - Java

GraphQLSchema schema = GraphQLSchema.newSchema()
    .query(StarWarsSchema.queryType)
    .fieldVisibility( NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY )
    .build();

禁用内省 & GraphiQL - JavaScript

app.use('/graphql', graphqlHTTP({
  schema: MySessionAwareGraphQLSchema,
+ validationRules: [NoIntrospection]
  graphiql: process.env.NODE_ENV === 'development',
}));

不要返回过多的错误信息

生产环境中的 GraphQL API 不应返回堆栈跟踪或处于调试模式。这与实现相关,但使用中间件是更好地控制服务器返回的错误的一种流行方式。要使用 Apollo Server 禁用过多错误,可以向 Apollo Server 构造函数传递 debug: false,或者将 NODE_ENV 环境变量设置为 'production' 或 'test'。但是,如果您想在内部记录堆栈跟踪而不将其返回给用户,请参阅此处了解如何屏蔽和记录错误,使其可供开发人员使用,但不对 API 调用者可见。

其他资源

工具

  • InQL Scanner - GraphQL 安全扫描器。尤其适用于根据给定模式自动生成查询和变更,然后将其馈送给扫描器。
  • GraphiQL - 模式/对象探索
  • GraphQL Voyager - 模式/对象探索

GraphQL 安全最佳实践 + 文档

更多 GraphQL 攻击