阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

如何保护你的 GraphQL API 免受恶意查询?

  • 2020-10-20
  • 本文字数:3560 字

    阅读完需:约 12 分钟

如何保护你的GraphQL API免受恶意查询?

使用GraphQL,你可以随时精确查询任何你想要的内容。对于 API 来说,这是令人惊奇的,但是也有复杂的安全隐患。恶意人员可能提交一个开销大的嵌套的查询,而不是请求合法的有用数据,来使你的服务器、数据库、网络或所有这些设施过载。没有正确的保护措施,你就会面临拒绝服务(Denial of Service,DoS)攻击的风险。


例如,在我们Spectrum的 GraphQL API 中,我们有一个如下关系:


type Thread {  messages(first: Int, after: String): [Message]}
type Message { thread: Thread}
type Query { thread(id: ID!): Thread}
复制代码


如你所见,你既可以查询一个线程的消息列表,也能查询一个消息的线程。这种循环关系让恶意人员可以构建一个如下的开销大的嵌套循环:


query maliciousQuery {  thread(id: "some-id") {    messages(first: 99999) {      thread {        messages(first: 99999) {          thread {            messages(first: 99999) {              thread {                # ...repeat times 10000...              }            }          }        }      }    }  }}
复制代码


如果让这种查询通过,那后果是非常糟糕的,因为它会指数级增加加载的对象数量并使你的整个服务器崩溃。尽管在其它层有一些缓解措施可以使得在第一时间发送这种查询有点儿困难(例如,CORS),但它们不能完全阻止这类查询的发生。

尺寸限制

我们考虑的第一种“天真”方案是根据原始字节数限制传入的查询大小。由于查询以字符串形式发送,因此一个快速的长度检查就足够了:


app.use('*', (req, res, next) => {  const query = req.query.query || req.body.query || '';  if (query.length > 2000) {    throw new Error('Query too large');  }  next();});
复制代码


不幸的是,这在现实世界不怎么生效:这个检查可能会让使用短字节名的恶意查询通过,阻止使用长字节名或嵌套结构的合法查询。

查询白名单

我们考虑的第二种方案是配置一个在我们自己的应用程序中认可的查询的白名单,告诉服务器不要让这些查询之外的任何查询通过。


app.use('/api', graphqlServer((req, res) => {  const query = req.query.query || req.body.query;  // TODO: Get whitelist somehow  if (!whitelist[query]) {    throw new Error('Query is not in whitelist.');  }  /* ... */}));
复制代码


手动维护这个认可的查询列表显示是一件痛苦的事,但值得庆幸的是,Apollo 团队创建了persistgraphql,它会从你的客户端代码中自动抽取所有查询并生成一个漂亮的 JSON 文件。


{  "scripts": {    "postbuild": "persistgraphql src api/query-whitelist.json"  }}
复制代码


这个技术很不错,能可靠地阻拦所有恶意查询。不幸的是,它也有两个主要权衡:


  1. 我们永远不能改变或删除查询,只能新增查询:如果任何用户运行一个过时的客户端,我们就不能阻拦他们的请求。我们将不得不维持生产环境曾经使用过的所有查询,而这会非常复杂。

  2. 我们不能将我们的 API 开放给公众:在将来的某个时候,我们希望将我们的 API 开放给公众,从而让其它开发者可以按自己的查询方式调用 Spectrum 的接口。如果我们只允许白名单中的查询通过,就会严重限制他们的查询选择,并且破坏了使用 GraphQL API 的意义(超级灵活的系统被一个人造白名单限制)。


这些都是我们无法接受的约束,所以我们只能回到原来的处境。

深度限制

上述恶意查询的一个有害方面就是嵌套,标志就是它的深度,这使得查询的开销呈现指数级增加。每一层都给你的后端增加了更多工作,当结合列表时可以快速增长。


我们环顾四周,发现了graphql-depth-limit,一个由Andrew Carlson开发的模块,让我们能轻易限制输入查询的最大深度。我们检查了客户端,发现使用的查询的最大深度为 7 层,因此我们设置最大深度为 10(相当宽大)并将它添加到我们的校验规则中:


app.use('/api', graphqlServer({  validationRules: [depthLimit(10)]}));
复制代码


深度限制就是这么简单!

数量限制

上述查询的第二个有害方面是获取 99999 个对象。无论这个对象是什么,获取大量的这个对象都会是开销巨大的。(尽管数据库压力可以通过 DataLoader 缓解,但网络和进程压力不会)


与其设置第一个参数类型为 Int(接受任意数字),我们用graphql-input-number创建了一个自定义标量,限制最大值为 100::


const PaginationAmount = GraphQLInputInt({  name: 'PaginationAmount',  min: 1,  max: 100,});
复制代码


如果任何人查询超过 100 个对象,这就会抛出一个错误。我们然后在任何使用连接的 API 的地方使用这个设置:


type Thread {  messages(first: PaginationAmount, after: String): [Message]}
复制代码


现在,我们已经完全阻止了上述恶意查询!

查询成本分析

不幸的是,在正确的情况下仍然有潜在的问题会使服务崩溃:有一些特定的 app 相关的查询,既不会太深,也不会请求太多对象,但是开销仍然会非常大。在 Spectrum,对我们来说,这样的查询可能是这样的:


query evilQuery {  thread(id: "54887141-57a9-4386-807c-ed950c4d5132") {    messageConnection(first: 100) { ... }    participants(first: 100) {      threadConnection(first: 100) { ... }      communityConnection { ... }      channelConnection { ... }      everything(first: 100) { ... }    }  }}
复制代码


深度或个体数量在这个查询中都不是特别高,因此它可以通过我们当前的保护措施。然而,它可能会获取成千上万条记录,这意味着它在数据库、服务器和网络上都是非常密集的,这是最坏的情况。


为阻止这种情况,我们需要在运行查询前对这些查询进行分析,计算它们的复杂度,如果它们的开销太大就阻止它们。这会比我们之前的保护措施更有效,能够 100%确保没有恶意查询能够到达我们的解析器。


在花费大量时间实现查询成本分析前,最好确定你需要它。尝试用恶意查询让你当前的 API 崩溃或变慢,看看你能做到什么程度——也许你的 API 并没有这种嵌套关系,或者它可以很好地处理一次性获取数千条记录,根本不需要查询成本分析!


我在自己的 2017 MacBook Pro 上本地运行上述查询,我们的 API 服务器用了 10-15 秒来响应一个兆级 JSON 数据。我们确实需要查询成本分析,因为我们不希望任何人用那个查询来“轰炸”我们的 API。(GitHub GraphQL API也使用了查询成本分析

实现查询成本分析

在 npm 上,有大量实现查询成本分析的包。我们的两个领先者是graphql-validation-complexity,一个即插即用模块,和graphql-cost-analysis,通过让你指定 @cost 指令来让你有更多控制能力。还有一个graphql-query-complexity,但我不推荐选择这个而不是 graphql-cost-analysis,因为两者想法类似,但它没有指令和乘法器支持。


我们使用graphql-cost-analysis,因为我们最快的解析器(20μs)和最慢的解析器(10s+)之间有巨大差异,因此我们需要它提供的控制能力。换句话说,graphql-validation-complexity 对你来说已经足够了。


它工作的方式是,你指定解析一个特定字段或类型的相对成本。它还支持乘法,因此,如果你请求了一个列表,其中包含的任何嵌套字段都会乘以页数,这非常简洁。


@cost 指令实际上是这样的:


type Participant {  # The complexity of getting one thread in a thread connection is 3, and multiply that by the amount of threads fetched  threadConnection(first: PaginationAmount, after: String): ThreadConnection @cost(complexity: 3, multipliers: ["first"])}type Thread {  author: Author @cost(complexity: 1)  participants(first: PaginationAmount,...): [Participant] @cost(complexity: 2, multipliers: ["first"])}
复制代码


这只是我们的 API 类的一部分,但是你可以明白指令是怎么样的了。你指定某个特定字段的复杂度,用于相乘,以及最大成本,然后 graphql-cost-analysis 会为了完成其余工作。


我通过Apollo Engine披露的性能跟踪数据来决定特定解析器的复杂度。我浏览了整个 schema,并根据 p99 服务的时间分配了一个值。然后,我们遍历客户端上所有的查询来找出开销最大的查询,这个查询的复杂度得分大约有 500。为了给我们未来留一点余地,我们将最大复杂度设为 750。


既然我们已经添加了 graphql-cost-analysis,运行上面的恶意查询,我得到了一个错误信息,告诉我“GraphQL 查询超过了最大复杂度,请删除一些嵌套或字段之后再重试。(最大:750,实际:1010319)”


一百万复杂度得分?拒绝!

总结

综上所述,我建议使用深度和数量限制作为任何 GraphQL API 的最小防护措施——它们易于实现而且能给予充足的安全保障。根据你具体的安全需求和架构,你可能需要研究查询成本分析。虽然它相比于其它工具需要更多工作,但它确实提供了针对恶意行为的全面防护。


原文链接:


https://www.apollographql.com/blog/securing-your-graphql-api-from-malicious-queries-16130a324a6b/


公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2020-10-20 10:521576
用户头像

发布了 165 篇内容, 共 71.6 次阅读, 收获喜欢 342 次。

关注

评论

发布
暂无评论
发现更多内容

浓缩即精华!腾讯云大神亲码“redis深度笔记”,堪称面试宝典!

收到请回复

Java 云计算 开源 架构 编程语言

阿里内部高产的 SpringBoot 保姆级笔记,面面俱到,太全了!

收到请回复

Java 云计算 开源 架构 编程语言

为啥是SQL?互联网投资回报比最高的技能是什么?

雨果

sql

3个轻量级物联网新品实验,带您深度体验IoT开发

华为云开发者联盟

物联网 沙箱实验 企业号九月金秋榜

聊聊数据库主键那点事儿

Steven

马蹄链Dapp系统开发(智能合约)

薇電13242772558

提高数据可视化效果的五个原则

博文视点Broadview

漏洞管理流程

SEAL安全

漏洞修复 漏洞管理 企业号九月金秋榜

面了个阿里拿38k出来的,让我见识到了基础顶端

程序知音

Java java面试 后端技术 秋招 八股文

堪称神作!啃透这份JVM笔记,轻松搞定阿里30K面试!!

收到请回复

Java 云计算 开源 架构 编程语言

流日志轻松应对“10亿级别IP对”复杂场景,实现超大规模混合云网络流量可视化

百度Geek说

运维 数据 流量 企业号九月金秋榜

SQL为什么历经半个世纪却经久不衰?

雨果

sql

MobTech短信验证ApiCloud端SDK

MobTech袤博科技

API 短信验证

两万字带你了解Java多线程(详细大总结)

Java快了!

中国的时区为什么是Asia/Shanghai,而不是Asia/Beijing?

Sher10ck

TiFlash 源码阅读(九)TiFlash 中常用算子的设计与实现

PingCAP

#TiDB TiDB 源码解读

区块链追溯:让冷链物流“热”起来!

旺链科技

区块链 产业区块链 企业号九月金秋榜 冷链物流

新书上市|一位家长的忠告:长大后不成才的孩子,父母都忽视了这个点!

图灵教育

育儿 教育 脑科学 基因

每日算法刷题Day14-反转链表、两个链表的第一个公共结点、删除链表中重复的节点

timerring

算法题 9月月更

大佬就是强!意外收获史诗级分布式资源,从基础到进阶,干货满满!

收到请回复

Java 云计算 开源 架构 编程语言

一文了解循环神经网络

华为云开发者联盟

人工智能 语音识别 企业号九月金秋榜

Redis数据倾斜与JD开源hotkey源码分析揭秘

京东科技开发者

数据库 数据倾斜 key Redis 数据结构 redis\

云原生底座之上,顺丰智慧供应链领跑的秘密

华为云开发者联盟

云计算 云原生 后端 企业号九月金秋榜

Java之static关键字的应用【工具类、代码块和单例】

Fire_Shield

static 9月月更 实际应用

快速体验 MicroK8s 开箱即用的服务网格

Flomesh

Service Mesh 服务网格

如何进行 Apache Doris 集群 Docker 快速部署

SelectDB

数据库 Doris Docker 镜像 安装 & 部署 企业号九月金秋榜

如何利用OpenHarmony ArkUI的Canvas组件实现涂鸦功能?

OpenHarmony开发者

OpenHarmony

2022年最新【Java经典面试800题】面试必备,查漏补缺:多线程+spring+JVM调优+分布式+redis+算法

收到请回复

Java 云计算 开源 架构 编程语言

带你体验给黑白照片上色

华为云开发者联盟

人工智能 华为云 图像 企业号九月金秋榜

SAP Cloud Application Programming 编程模型(CAP)的设计准则

Jerry Wang

CAP Cloud SAP Cloud Studio 9月月更

新书上市|一位家长的忠告:长大后不成才的孩子,父母都忽视了这个点!

图灵社区

育儿 教育 脑科学 基因

如何保护你的GraphQL API免受恶意查询?_安全_Max Stoiber_InfoQ精选文章