# GraphQL漏洞

## GraphQL概述

[GraphQL](https://spec.graphql.org/) 是一种查询语言，旨在通过提供直观灵活的语法和系统来描述其数据需求和交互来构建客户端应用程序。GraphQL使用声明式方法获取数据，客户端可以准确指定它们需要从API获取什么数据。因此，GraphQL提供了单个端点，允许客户端获取必要的数据，而不是REST API中的多个端点。

![](https://2241975275-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7lonY2zgW7DNoIQMqYtw%2Fuploads%2Fgit-blob-8058fb9c0584cd92861f782de67062a618839f79%2Fgraphql-vs-rest.jpeg?alt=media)

### GraphQL模式

GraphQL服务器使用模式来描述可用数据的形状。此模式定义了一个类型层次结构，其中包含从后端数据存储填充的字段。模式还确切指定了客户端可以执行的查询和变更。

大多数模式类型都有一个或多个 `fields`。每个字段返回指定的 `type` 的数据。GraphQL模式中的每个类型定义都属于以下类别之一：

* [Scalar](#scalar-types)
* [Object](#object-types)，这包括三个特殊的 `root operation types`：
  * [Query](#query-types)
  * [Mutation](#mutation-types)
  * [Subscription](#subscription-types)
* [Input](#input-types)
* [Enum](#enum-types)
* [Union](#union-types)
* [Interface](#interface-types)

#### 标量类型

标量类型总是解析为具体数据。GraphQL提供以下默认标量类型：

* `Int` 有符号32位整数
* `Float` 有符号双精度浮点值
* `String` UTF-8字符序列
* `Boolean` true或false
* `ID`（序列化为 `String`）唯一标识符，通常用于重新获取对象或作为缓存的键。虽然序列化为String，但ID不打算为人类可读。

这些基本类型涵盖了大多数用例。但是，可以创建[自定义标量类型](https://graphql.org/learn/schema/#scalar-types)。

#### 对象类型

GraphQL模式中的大多数类型都是对象类型。对象类型包含 `fields` 的集合，每个字段都有自己的 `type`。两个对象类型可以相互包含作为字段。

#### 查询类型

`Query` 类型是一种特殊的对象类型，定义了客户端对服务器执行的所有查询的顶级入口点。Query类型的每个字段定义了不同入口点的名称和返回类型。

您可以使用Query类型如下获取有关用户的数据：

```graphql
/**
 * 请求所有用户的名称
 * 您也可以请求另一个字段，例如id或email
 */
query {
  allUsers {
    name
  }
}

/**
 * 请求id为1337的用户的名称
 * 您也可以请求另一个字段，例如id或email
 */
query {
  allUsers(id: 1337) {
    name
  }
}
```

{% hint style="info" %}
Query操作中的所有字段都是并行请求的。
{% endhint %}

```graphql
/**
 * getUserById和getUserByName将并行请求
 */
query {
  getUserById { ... }
  getUserByName { ... }
}
```

#### 变更类型

`Mutation` 类型的结构和目的与Query类型相似。Query类型定义读取操作的入口点，而Mutation类型定义 `write` 操作的入口点。此类型是可选的。Mutation类型的每个字段定义了不同入口点的签名和返回类型。

您可以使用Mutation类型如下创建新用户：

```graphql
/**
 * 创建新用户
 * 在响应中请求id、name和email字段
 */
mutation {
  createUser(name:"User", email: "user@website.com") {
    id
    name
    email
  }
}
```

{% hint style="info" %}
Mutation操作中的字段是顺序请求的。
{% endhint %}

```graphql
/**
 * 操作按以下顺序调用：
 *   1. createUser
 *   2. removeLastUser
 */
mutation {
  createUser { ... }
  removeLastUser { ... }
}
```

#### 订阅类型

Subscription类型用于通知用户系统中发生的任何更改。此类型是可选的。

Subscription类型的工作方式如下：

* 客户端订阅某个操作并与服务器建立连接（通常通过WebSocket）。
* 当此操作发生时，服务器通过创建的连接发送通知。

您可以订阅创建新用户如下：

```graphql
/**
 * 当新用户创建时
 * 服务器将新用户的名称和email发送给客户端
 */
subscription {
  newUser {
    name
    email
  }
}
```

#### 输入类型

`Input` 类型是特殊的对象类型，允许您将分层数据作为参数提供给字段（而不是仅提供平面标量参数）。输入类型的每个字段只能是标量、枚举或其他输入类型。

#### 枚举类型

`Enum` 类似于标量类型，但其合法值在模式中定义。枚举在用户必须从规定选项列表中选择的情况下最有用。

#### 联合类型

`Union` 类型声明哪些对象类型包含在联合中。字段可以拥有联合（或该联合的列表）作为其返回类型。在这种情况下，它可以返回包含在联合中的任何对象类型。

联合的所有包含类型必须是对象类型（不是标量、输入类型等）。

#### 接口类型

接口指定多个对象类型可以包含的一组字段。如果对象类型实现接口，它必须包含该接口的所有字段。

字段可以拥有接口（或该接口的列表）作为其返回类型。在这种情况下，它可以返回实现该接口的任何对象类型。

### 内省模式

GraphQL定义了内省模式，用于向GraphQL查询关于它支持哪些查询的信息。您可以使用以下查询获取内省模式：

您也可以使用各种GraphQL IDE或GraphQL Voyager进行内省。

{% embed url="https://github.com/APIs-guru/graphql-voyager" %}

但是，开发者可以禁止他们的应用程序进行内省。在这种情况下，您可以尝试使用 `clairvoyance` 获取模式。

{% embed url="https://github.com/nikitastupin/clairvoyance" %}

#### 如何读取内省模式？

通常，内省模式看起来如下：

```json
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      },
      "mutationType": {
        "name": "Mutation"
      },
      "subscriptionType": {
        "name": "Subscription"
      },
      "types": [ ... ],
      "directives": [ ... ]
    }
  }
}
```

* `queryType`、`mutationType`、`subscriptionType` 定义包含应用程序支持的相应查询列表的字段名称。换句话说，如果 `queryType` 名称是 `QueryRoot`，您可以在 `data.__schema.types` 列表中找到名称为 `QueryRoot` 的元素内的所有支持的Query类型。
* `types` 包含所有支持的变量和查询。
* `directives` 包含支持的[指令](https://graphql.org/learn/queries/#directives)列表。

例如，以下模式定义了 `User` 对象和返回 `Users` 列表的 `allUsers` 查询：

```json
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "QueryRoot"
      },
      "types": [
        {
          "name": "User",
          "kind": "OBJECT",
          "fields": [
            {
              "name": "name",
              "type": {
                "name": "String",
                "kind": "SCALAR"
              }
            }
          ]
        },
        {
          "name": "QueryRoot",
          "kind": "OBJECT",
          "fields": [
            {
              "name": "allUsers",
              "description": "Returns all users.",
              "args": [],
              "type": {
                "name": null,
                "kind": "LIST",
                "ofType": {
                  "name": "User",
                  "kind": "OBJECT",
                  "ofType": null
                }
              }
            }
          ]
        }
      ]
    }
  }
}
```

## 安全问题

### 滥用GraphQL作为API网关

GraphQL解析器可以实现为REST API网关，并使用提供的参数来制作和发送请求到API。如果参数未正确验证，解析器可能容易受到SSRF攻击。

假设，GrapghQL方案包含一个通过ID提供用户信息的查询：

```graphql
type Query {
    userByID(id: ID!):User
}

type User {
    id: ID!
    name: String!
    friends: [User!]!
}
```

解析器可能看起来像这样（伪代码）：

```python
results = client.get('https://api.website.internal/users/{id}')
return result.data
```

在这种情况下，您可以使用以下查询发送请求：

```graphql
{
    firstFriend: userByID(id: "1337/friends/1"){
        id
        name
    }
    secondFriend: userByID(id: "1337/friends/2"){
        id
        name
    }
}
```

这导致以下GET请求：

```http
https://api.website.internal/users/1337/friends/1
https://api.website.internal/users/1337/friends/2
```

这是可能的，因为ID标量应该[序列化为字符串](https://spec.graphql.org/June2018/#sec-ID)，而"1337/friends/1"是有效的字符串。

### 滥用GraphQL引擎

GraphQL引擎用于实现GraphQL API。引擎可能存在漏洞或配置错误。

为了确定使用哪个引擎，您可以使用 `graphw00f`。

{% embed url="https://github.com/dolevf/graphw00f" %}

### 损坏的访问控制

GraphQL在设计上不定义任何访问控制。开发者在解析方法内实现访问控制和业务逻辑代码。因此，尝试使用REST API情况下使用的技术绕过访问控制检查。

参考资料：

* [Report: Confidential data of users and limited metadata of programs and reports accessible via GraphQL](https://hackerone.com/reports/489146)
* [Report: Insufficient Type Check leading to Developer ability to delete Project, Repository, Group, ...](https://gitlab.com/gitlab-org/gitlab/-/issues/239348)
* [Report: Insufficient Type Check on GraphQL leading to Maintainer delete repository](https://gitlab.com/gitlab-org/gitlab/-/issues/215703)
* 工具：[AutoGraphQL](https://graphql-dashboard.herokuapp.com/) + [使用指南](https://www.youtube.com/watch?v=JJmufWfVvyU)

### 绕过CSRF保护

#### 通过[@inhibitor181](https://mobile.twitter.com/intigriti/status/1416363312296734720)更改Content-Type

尝试更改 `Content-Type` 头以获取CSRF：

```http
POST /api/graphql HTTP/1.1
Content-Type: application/json

{"query":"mutation ..."}
```

```http
POST /api/graphql HTTP/1.1
Content-Type: application/x-www-form-urlencoded

query=mutation...
```

#### 更改HTTP方法

尝试发送 `GET` 请求而不是 `POST` 请求以获取CSRF。

参考资料：

* [Report: CSRF on /api/graphql allows executing mutations through GET requests](https://hackerone.com/reports/1122408)

### 绕过速率限制

GraphQL规范允许通过将多个请求批处理在一起在单个请求中发送它们。如果开发者没有实现某种机制来阻止发送批处理请求，您可以通过在单个请求中发送查询来潜在地绕过速率限制。

```graphql
mutation { login(input: { user:"a", password:"password" }) { success } }
mutation { login(input: { user:"b", password:"password" }) { success } }
....
mutation { login(input: { user:"z", password:"password" }) { success } }
```

### 拒绝服务

默认情况下GraphQL不限制查询的长度。GraphQL查询可以相互嵌套并创建对数据库的级联请求。因此，可以执行嵌套查询以导致拒绝服务攻击：

```graphql
query {
  posts {
    title
    comments {
      comment
      user {
        comments {
          user {
            comments {
              comment
              user {
                comments {
                  comment
                  user {
                    comments {
                      comment
                      user {
                      ...
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

### 过度错误

GraphQL具有[返回错误的良好且富有表现力的方式](https://spec.graphql.org/June2018/#sec-Errors)。但是，错误消息可能过于信息丰富。尝试通过模糊测试参数等导致错误，错误消息可能揭示有关错误、系统上的实际路径、代码或查询块等的详细信息。

### GraphQL注入

尽管GraphQL是强类型的，SQL、NoSQL和命令注入仍然是可能的，因为GraphQL只是客户端和后端之间的层。

```graphql
mutation { 
    login(input: {
        user: "admin", 
        password: "password' or 1=1 -- -"
    }) { 
        success
    } 
}

mutation {
    users(search: "{password: { $regex: \".*\"}, name:Admin }") {
        id
        name
        password
    }
}
```

### 信息泄露

通常GraphQL API暴露大量信息，如私人数据、调试信息、隐藏数据或堆栈跟踪。

参考资料：

* [Team object in GraphQL disclosed total number of whitelisted hackers](https://hackerone.com/reports/342978)
* [Team object exposes amount of participants in a private program to non-invited users](https://hackerone.com/reports/380317)

## 参考资料

* [GraphQL规范](https://spec.graphql.org/)
* [Practical GraphQL attack vectors](https://jondow.eu/practical-graphql-attack-vectors/)
* [Public GraphQL APIs](https://github.com/APIs-guru/graphql-apis)
* [Apollo Docs: GraphQL schema basics](https://www.apollographql.com/docs/apollo-server/schema/schema/)
