在我看来,graphql提供了一种api的解决方案。
以前我们查询api提供的数据是怎么做的呢?
举一个例子,例如现在有这么一个查询书籍信息的接口:/book/info,按照我们之前的解决方案,我们一般会传一个类似book_id的参数到这个接口,然后接口把书籍信息返回给我们
同样的,如果又有一个查询作者信息的需求,那么我们会新增一个接口:/author/info
,并以book_id为参数查询作者信息
可以看到,以往我们使用的api查询数据的操作是分散的,彼此之间是没啥联系,查询多个数据就要向不同的接口发送多个请求
但是graphql很特别,它一般只提供一个接口/graphql
,所有的数据查询操作都是向此接口发送请求,并且,只需要发送一次请求就可以获得多个数据
那graphql是怎么实现这一点的呢?
这得归功于graphql的面向对象思想
在graphql中,所有的数据形成了一张彼此相互关联的“图”,此处的“图”是指数据结构中的“图”
怕大家忘了啥是图,贴一张图回忆回忆
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。图论是一种表示 "多对多" 的关系 图是由顶点和边组成的:(可以无边,但至少包含一个顶点)
图中的每一个顶点就是咱们的数据,那这些数据是怎么关联起来的呢?
实际上还是借助了面向对象的思想,graphql把每一种数据都给抽象成了一个类,例如咱们上面提到的两个接口,在graphql中,这两个接口背后对应的数据就可以被抽象为Book类以及Author类
但是,这里说的类只是一个概念,区别于咱们编程语言中的类,与其说它是一个类,不如说它是一个结构,在Graphql中,Book类可以这样描述
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
Author类:
type Author {
id: ID
firstName: String
lastName: String
book: Book
}
其中Book类有一个属性是author,代表一本书的作者,而Author类中有一个属性是book,表示该作者写过的所有的书,这两个类一下子就产生了联系
如果用图论中的有向图表示这两个顶点,就是下面这个样子:
现在咱们只是对数据做了抽象描述,那么怎么把数据暴露给前端呢?咱们还需要借助一个类型
这个类型就是Query,在Query类中我们可以定义各个查询接口:
type Query {
bookById(id: ID): Book
authorById(id: ID): Author
}
上面就在Query类中定义了两个接口,bookById(id: ID): Book
表示对外暴露的接口名为bookById,然后这个接口接受一个参数id
,这个id的类型为ID
,该接口的返回类型为Book
暴露了接口,我们前端就可以查询了,前端查询的格式也是很讲究的,例如我要查id为book-1
的书籍的名字和页数,就可以传如下数据到接口/graphql
:
{
bookByID(id: "book-1"){
name,
pageCount
}
}
如果我要查,id为book-1
的书籍的名字以及其作者的名字,可以发送如下格式的数据:
{
bookByID(id: "book-1"){
name,
author{
firstName,
lastName
}
}
}
返回结果也是很清晰的,如下:
总结起来,graphql的工作流程如下:
当然,这也只是一个整体上的流程,没有涉及到具体的代码实现,其实在写代码的时候还得考虑数据来自哪里,这就涉及到为每个数据类型配置dataFetcher了,具体的代码实现案例可以看官网的入门教程(java版),不再赘述:
https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/#author-datafetcher
扯了这么多,那graphql都面临着哪些安全风险呢?
graphiql实际上就是一个graphql的调试工具,有了该调试工具可以更方便我们执行graphql语句,但是对攻击者来说如果一个程序对外提供了/graphql
接口,开没开graphiql接口也就没那么重要了
因为graphiql接口能做的,graphql接口也可以做到?
内省官方文档:https://graphql.cn/learn/introspection/
graphql内省机制涉及到的最大的问题就是信息泄露了,如果系统没有对内省机制进行处理,则可能会泄露系统中所有的可用的查询,以及查询支持的字段等等,也就是说整个系统处于裸奔状态
如果系统中有一些敏感查询,则会泄露很多信息,甚至影响正常的业务逻辑
graphql支持的内省查询有两个__schema、__type
,其实使用__schema
就够用了
查询graphql中注册的所有类型:
可以看到查询到了我们之前注册的Book以及Author类型,但是想要进一步利用,只是知道系统中注册的所有类型还不行,我们需要知道系统支持哪些查询,用如下语句:
可以看到,我们之前注册的两个查询也出来了,但是还不够,咱们还得知道这些查询下支持哪些字段以及是否需要参数:
可以看到成功查询出了bookById的查询的参数是id,返回类型是Book,然后我们在去看一下Book的字段就知道bookByID可以查询哪些字段了:
可以看到,Book下的字段有id,name,pageCount,author,那么我们就可以直接构造查询了:
上述过程就是一个完整的graphql内省机制的利用流程,我这里演示的接口就是单纯的查询信息,没啥危害,但是要知道真实环境中的graphql api可不只是用来做查询操作的,还有各种写操作,造成的危害也就上了一个层次
规避方案:目前还不了解graphql是否支持关闭内省机制,但是我们可以在业务上对内省机制进行限制,例如过滤查询中的__schema、__type
关键词,这两个关键词都是小写,所以暂时也不需要担心大写绕过的问题(但是业务上对大小写进行了不正确的转换就另说了)
自动绑定原理其实很简单,和spring中的自动绑定机制有点类似,就是如果我爆破出了你的敏感接口、以及对应的字段,我就可以直接使用,这本就是graphql的一个特点,不算是漏洞,但是这确实会带来一些安全问题
规避方案:
graphql实现了一套自己的查询语言,和spring中的el类似,graphql也是存在表达式注入的问题
漏洞原理:前端传入的参数未正确处理就直接拼接到了graphql语句中,例如下面这样的场景
String book_id = req.getParameter("book_id");
String gql_query = "{bookById(id:\""+book_id+"\"){name,pageCount}}";
用gql_query执行查询
现在如果我给book_id赋值为book-1"){name}authorById("author-1"){firstName}#
gql_query的值就变为:{bookById(id:"book-1"){name}authorById("author-1"){firstName}#"){name,pageCount}}
其中,#
是注释符,经过拼接,查询的语义完全变了
当然,这样使用graphql的场景应该也不多,毕竟这已经违背了graphql的设计理念...
其实我上面给的案例的那种写法就存在着Dos隐患,问题出在两个类型互相关联上,Book中有Author,Author中有Book,当数据量够大时,这样我们可以直接构造多层查询达到Dos效果
类似于xee攻击:https://www.cnblogs.com/lcamry/p/5737318.html
鉴权问题是api通用的问题
只是开发者在使用graphql的过程中更容易忽略对敏感接口进行细粒度的鉴权,因为graphql是单路由的api,所以,开发者往往也只是对这个路由进行了权限判断
但是实际上graphql中注册的各个查询可能要求的权限是不一样的
例如只有admin权限才能调用的查询addMoney
,就需要在业务上限制非admin用户不能访问