热门标签 | 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(`

推荐阅读
  • Kubernetes(k8s)基础简介
    Kubernetes(k8s)基础简介目录一、Kubernetes概述(一)、Kubernetes是什么(二& ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 本文比较了eBPF和WebAssembly作为云原生VM的特点和应用领域。eBPF作为运行在Linux内核中的轻量级代码执行沙箱,适用于网络或安全相关的任务;而WebAssembly作为图灵完备的语言,在商业应用中具有优势。同时,介绍了WebAssembly在Linux内核中运行的尝试以及基于LLVM的云原生WebAssembly编译器WasmEdge Runtime的案例,展示了WebAssembly作为原生应用程序的潜力。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 恶意软件分析的最佳编程语言及其应用
    本文介绍了学习恶意软件分析和逆向工程领域时最适合的编程语言,并重点讨论了Python的优点。Python是一种解释型、多用途的语言,具有可读性高、可快速开发、易于学习的特点。作者分享了在本地恶意软件分析中使用Python的经验,包括快速复制恶意软件组件以更好地理解其工作。此外,作者还提到了Python的跨平台优势,使得在不同操作系统上运行代码变得更加方便。 ... [详细]
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社区 版权所有