热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:GraphQL安全指北

篇首语:本文由编程笔记#小编为大家整理,主要介绍了GraphQL安全指北相关的知识,希望对你有一定的参考价值。*在616先知白帽

篇首语:本文由编程笔记#小编为大家整理,主要介绍了GraphQL安全指北相关的知识,希望对你有一定的参考价值。


* 在616先知白帽大会上听到@phith0n大佬的议题《攻击GraphQL》,从攻击者视角描述了GraphQL的攻击面。让我想起之前在做某个项目时,鬼使神差的(其实是健忘症又犯了)学习并尝试了GraphQL这个还没完全火起来但又有很多大厂使用的Web API技术,当时和好基友@图南也对其安全性相关问题存在的疑虑做了很多探讨和研究,于是决定和他联名合作完成这篇关于GraphQL安全的文章。我俩水平有限,不足之处请批评指正。




说在前面的话

本文以GraphQL中一些容易让初学者与典型Web API(为了便于理解,下文以目前流行的RESTful API为例代指)混淆或错误理解的概念特性进行内容划分,由我从安全的角度抛出GraphQL应该注意的几点安全问题,而@图南则会更多的从开发的角度给出他在实际使用过程中总结的最佳实践。

另外,需要提前声明的是,本文中我使用的后端开发语言是Go,@图南使用的是Node.js,前端统一为React(GraphQL客户端为Apollo),请大家自行消化。

Let’s Go!


GraphQL简介

有些同学是不是根本没听过这个玩意?我们先来看看正在使用它的大客户们:


是不是值得我们花几分钟对它做个简单的了解了?XD


什么是GraphQL

简单的说,GraphQL是由Facebook创造并开源的一种用于API的查询语言。

GraphQL安全指北

再引用官方文案来帮助大家理解一下GraphQL的特点:



  • 请求你所要的数据,不多不少
    向你的API发出一个GraphQL请求就能准确获得你想要的数据,不多不少。GraphQL查询总是返回可预测的结果。使用GraphQL的应用可以工作得又快又稳,因为控制数据的是应用,而不是服务器


  • 获取多个资源,只用一个请求
    GraphQL查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询。典型的RESTful API请求多个资源时得载入多个URL,而GraphQL可以通过一次请求就获取你应用所需的所有数据


  • 描述所有的可能,类型系统
    GraphQL基于类型和字段的方式进行组织,而非入口端点。你可以通过一个单一入口端点得到你所有的数据能力。GraphQL使用类型来保证应用只请求可能的数据,还提供了清晰的辅助性错误信息



GraphQL核心组成部分



  • Type
    用于描述接口的抽象数据模型,有Scalar(标量)和Object(对象)两种,Object由Field组成,同时Field也有自己的Type


  • Schema
    用于描述接口获取数据的逻辑,类比RESTful中的每个独立资源URI


  • Query
    用于描述接口的查询类型,有Query(查询)、Mutation(更改)和Subscription(订阅)三种


  • Resolver
    用于描述接口中每个Query的解析逻辑,部分GraphQL引擎还提供Field细粒度的Resolver


(想要详细了解的同学请阅读GraphQL官方文档)


GraphQL VS. RESTful

GraphQL没有过多依赖HTTP协议,它有一套自己的解析引擎来帮助前后端使用GraphQL查询语法。同时它是单路由形态,查询内容完全根据前端请求对象和字段而定,前后端分离较明显。

用一张图来对比一下:

GraphQL安全指北


身份认证与权限控制不当



@gyyyy:
前面说到,GraphQL多了一个中间层对它定义的查询语言进行语法解析执行等操作,与RESTful这种充分利用HTTP协议本身特性完成声明使用的API设计不同,Schema、Resolver等种种定义会让开发者对它的存在感知较大,间接的增加了对它理解的复杂度,加上它本身的单路由形态,很容易导致开发者在不完全了解其特性和内部运行机制的情况下,错误实现甚至忽略API调用时的授权鉴权行为。


在官方的描述中,GraphQL和RESTful API一样,建议开发者将授权逻辑委托给业务逻辑层:

GraphQL安全指北

在没有对GraphQL中各个Query和Mutation做好授权鉴权时,同样可能会被攻击者非法请求到一些非预期接口,执行高危操作,如查询所有用户的详细信息:

query GetAllUsers {
users {
_id
username
password
idCard
mobilePhone
email
}
}

这几乎是使用任何API技术都无法避免的一个安全问题,因为它与API本身的职能并没有太大的关系,API不需要背这个锅,但由此问题带来的并发症却不容小觑。


信息泄露

对于这种未授权或越权访问漏洞的挖掘利用方式,大家一定都很清楚了,一般情况下我们都会期望尽可能获取到比较全量的API来进行进一步的分析。在RESTful API中,我们可能需要通过代理、爬虫等技术来抓取API。而随着Web 2.0时代的到来,各种强大的前端框架、运行时DOM事件更新等技术使用频率的增加,更使得我们不得不动用到如Headless等技术来提高对API的获取覆盖率。

但与RESTful API不同的是,GraphQL自带强大的内省自检机制,可以直接获取后端定义的所有接口信息。比如通过__schema查询所有可用对象:

{
__schema {
types {
name
}
}
}

通过__type查询指定对象的所有字段:

{
__type(name: "User") {
name
fields {
name
type {
name
}
}
}
}

这里我通过graphql-go/graphql的源码简单分析一下GraphQL的解析执行流程和内省机制,帮助大家加深理解:



  • GraphQL路由节点在拿到HTTP的请求参数后,创建Params对象,并调用Do()完成解析执行操作返回结果:


params := graphql.Params{
Schema: *h.Schema,
RequestString: opts.Query,
VariableValues: opts.Variables,
OperationName: opts.OperationName,
Context: ctx,
}
result := graphql.Do(params)


  • 调用Parser()params.RequestString转换为GraphQL的AST文档后,将AST和Schema一起交给ValidateDocument()进行校验(主要校验是否符合Schema定义的参数、字段、类型等)


  • 代入AST重新封装ExecuteParams对象,传入Execute()中开始执行当前GraphQL语句


具体的执行细节就不展开了,但是我们关心的内省去哪了?原来在GraphQL引擎初始化时,会定义三个带缺省Resolver的元字段:

SchemaMetaFieldDef = &FieldDefinition{ // __schema:查询当前类型定义的模式,无参数
Name: "__schema",
Type: NewNonNull(SchemaType),
Description: "Access the current type schema of this server.",
Args: []*Argument{},
Resolve: func(p ResolveParams) (interface{}, error) {
return p.Info.Schema, nil
},
}
TypeMetaFieldDef = &FieldDefinition{ // __type:查询指定类型的详细信息,字符串类型参数`name`
Name: "__type",
Type: TypeType,
Description: "Request the type information of a single type.",
Args: []*Argument{
{
PrivateName: "name",
Type: NewNonNull(String),
},
},
Resolve: func(p ResolveParams) (interface{}, error) {
name, ok := p.Args["name"].(string)
if !ok {
return nil, nil
}
return p.Info.Schema.Type(name), nil
},
}
TypeNameMetaFieldDef = &FieldDefinition{ // __typename:查询当前对象类型名称,无参数
Name: "__typename",
Type: NewNonNull(String),
Description: "The name of the current Object type at runtime.",
Args: []*Argument{},
Resolve: func(p ResolveParams) (interface{}, error) {
return p.Info.ParentType.Name(), nil
},
}

resolveField()解析到元字段时,会调用其缺省Resolver,触发GraphQL的内省逻辑。


自动绑定(非预期和废弃字段)

GraphQL为了考虑接口在版本演进时能够向下兼容,还有一个对于应用开发而言比较友善的特性:『API演进无需划分版本』。

由于GraphQL是根据前端请求的字段进行数据回传,后端Resolver的响应包含对应字段即可,因此后端字段扩展对前端无感知无影响,前端增加查询字段也只要在后端定义的字段范围内即可。同时GraphQL也为字段删除提供了『废弃』方案,如Go的graphql包在字段中增加DeprecationReason属性,Apollo的@deprecated标识等。

这种特性非常方便的将前后端进行了分离,但如果开发者本身安全意识不够强,设计的API不够合理,就会埋下了很多安全隐患。我们用开发项目中可能会经常遇到的需求场景来重现一下。

假设小明在应用中已经定义好了查询用户基本信息的API:

graphql.Field{
Type: graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Description: "用户信息",
Fields: graphql.Fields{
"_id": &graphql.Field{Type: graphql.Int},
"username": &graphql.Field{Type: graphql.String},
"email": &graphql.Field{Type: graphql.String},
},
}),
Args: graphql.FieldConfigArgument{
"username": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: func(params graphql.ResolveParams) (result interface{}, err error) {
// ...
},
}

小明获得新的需求描述,『管理员可以查询指定用户的详细信息』,为了方便(也经常会为了方便),于是在原有接口上新增了几个字段:

graphql.Field{
Type: graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Description: "用户信息",
Fields: graphql.Fields{
"_id": &graphql.Field{Type: graphql.Int},
"username": &graphql.Field{Type: graphql.String},
"password": &graphql.Field{Type: graphql.String}, // 新增 用户密码 字段
"idCard": &graphql.Field{Type: graphql.String}, // 新增 用户身份证号 字段
"mobilePhone": &graphql.Field{Type: graphql.String}, // 新增 用户手机号 字段
"email": &graphql.Field{Type: graphql.String},
},
}),
Args: graphql.FieldConfigArgument{
"username": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: func(params graphql.ResolveParams) (result interface{}, err error) {
// ...
},
}

如果此时小明没有在字段细粒度上进行权限控制(也暂时忽略其他权限问题),攻击者可以轻易的通过内省发现这几个本不该被普通用户查看到的字段,并构造请求进行查询(实际开发中也经常容易遗留一些测试字段,在GraphQL强大的内省机制面前这无疑是非常危险的。如果熟悉Spring自动绑定漏洞的同学,也会发现它们之间有一部分相似的地方)

故事继续,当小明发现这种做法欠妥时,他决定废弃这几个字段:

// ...
"password": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性问题"},
"idCard": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性问题"},
"mobilePhone": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性问题"},
// ...

接着,他又用上面的__type做了一次内省,很好,废弃字段查不到了,通知前端回滚查询语句,问题解决,下班回家(GraphQL的优势立刻凸显出来)

熟悉安全攻防套路的同学都知道,很多的攻击方式(尤其在Web安全中)都是利用了开发、测试、运维的知识盲点(如果你想问这些盲点的产生原因,我只能说是因为正常情况下根本用不到,所以不深入研究基本不会去刻意关注)。如果开发者没有很仔细的阅读GraphQL官方文档,特别是内省这一章节的内容,就可能不知道,通过指定includeDeprecated参数为true__type仍然可以将废弃字段暴露出来:

{
__type(name: "User") {
name
fields(includeDeprecated: true) {
name
isDeprecated
type {
name
}
}
}
}

而且由于小明没有对Resolver做修改,废弃字段仍然可以正常参与查询(兼容性惹的祸),故事结束。

正如p牛所言,『GraphQL是一门自带文档的技术』。可这也使得授权鉴权环节一旦出现纰漏,GraphQL背后的应用所面临的安全风险会比典型Web API大得多。



@图南:
GraphQL并没有规定任何身份认证和权限控制的相关内容,这是个好事情,因为我们可以更灵活的在应用中实现各种粒度的认证和权限。但是,在我的开发过程中发现,初学者经常会忽略GraphQL的认证,会写出一些裸奔的接口或者无效认证的接口。那么我就在这里详细说一下GraphQL的认证方式。



独立认证终端(RESTful)

如果后端本身支持RESTful或者有专门的认证服务器,可以修改少量代码就能实现GraphQL接口的认证。这种认证方式是最通用同时也是官方比较推荐的。

以JWT认证为例,将整个GraphQL路由加入JWT认证,开放两个RESTful接口做登录和注册用,登录和注册的具体逻辑不再赘述,登录后返回JWT Token:

// ...
router.post('/login', LoginController.login);
router.post('/register', LoginController.register);
app.use(koajwt({secret: 'your secret'}).unless({
path: [/^\/public/, '/login', '/register']
}));
const server = new ApolloServer({
typeDefs: schemaText,
resolvers: resolverMap,
context: ({ctx}) => ({
...ctx,
...app.context
})
});
server.applyMiddleware({app});
app.listen({
port: 4000
}, () => console.log(`

推荐阅读
  • REST API 时代落幕,GraphQL 持续引领未来
    尽管REST API已广泛使用多年,但在深入了解GraphQL及其解决的核心问题后,我深感其将引领未来的API设计趋势。GraphQL不仅提高了数据查询的效率,还增强了灵活性和性能,有望成为API开发的新标准。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 本文介绍了Spring 2.0引入的TaskExecutor接口及其多种实现,包括同步和异步执行任务的方式。文章详细解释了如何在Spring应用中配置和使用这些线程池实现,以提高应用的性能和可管理性。 ... [详细]
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • Python错误重试让多少开发者头疼?高效解决方案出炉
    ### 优化后的摘要在处理 Python 开发中的错误重试问题时,许多开发者常常感到困扰。为了应对这一挑战,`tenacity` 库提供了一种高效的解决方案。首先,通过 `pip install tenacity` 安装该库。使用时,可以通过简单的规则配置重试策略。例如,可以设置多个重试条件,使用 `|`(或)和 `&`(与)操作符组合不同的参数,从而实现灵活的错误重试机制。此外,`tenacity` 还支持自定义等待时间、重试次数和异常处理,为开发者提供了强大的工具来提高代码的健壮性和可靠性。 ... [详细]
  • TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得
    TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得 ... [详细]
  • 作为140字符的开创者,Twitter看似简单却异常复杂。其简洁之处在于仅用140个字符就能实现信息的高效传播,甚至在多次全球性事件中超越传统媒体的速度。然而,为了支持2亿用户的高效使用,其背后的技术架构和系统设计则极为复杂,涉及高并发处理、数据存储和实时传输等多个技术挑战。 ... [详细]
  • NoSQL数据库,即非关系型数据库,有时也被称作Not Only SQL,是一种区别于传统关系型数据库的管理系统。这类数据库设计用于处理大规模、高并发的数据存储与查询需求,特别适用于需要快速读写大量非结构化或半结构化数据的应用场景。NoSQL数据库通过牺牲部分一致性来换取更高的可扩展性和性能,支持分布式部署,能够有效应对互联网时代的海量数据挑战。 ... [详细]
  • EMURGO Africa 与 Adaverse 合作投资 Momint,推动 Cardano NFT 生态系统在非洲市场的扩展 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 在2019中国国际智能产业博览会上,百度董事长兼CEO李彦宏强调,人工智能应务实推进其在各行业的应用。随后,在“ABC SUMMIT 2019百度云智峰会”上,百度展示了通过“云+AI”推动AI工业化和产业智能化的最新成果。 ... [详细]
  • 利用 Python 实现 Facebook 账号登录功能 ... [详细]
  • 如何有效解决MySQL中预编译语句失效的问题及专业应对策略 ... [详细]
  • vue github开源项目_2018 年最好的 45 个 Vue 开源项目汇总
    vuegithub开源项目_2018年最好的45个Vue开源项目汇总,Go语言社区,Golang程序员人脉社 ... [详细]
  • SocialFi 的未来:数据所有权、更公平的价值分配和行为数据的价值化
    SocialFi本质上是对Web2中心化社交平台的一次价值解构。撰文:Morty ... [详细]
author-avatar
mobiledu2502872283
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有