立即领取|华润集团、宁德核电、东风岚图等 20+ 标杆企业数字化人才培养实践案例 了解详情
写点什么

如何保护你的 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/


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

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

关注

评论

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

TiDB的数据自动均衡到底是怎么实现的?

TiDB 社区干货传送门

数据库架构设计 TiKV 底层架构

PCSD考试说明及课程汇总

TiDB 社区干货传送门

社区活动 OLTP 场景实践 7.x 实践 学习&认证&课程

测试开发名企定向培训训练营即将开营,限时优惠进行中

测试人

软件测试

深入大模型的世界

我是谁

预见预判|AIRIOT智慧交通管理解决方案

AIRIOT

智慧城市交通 智能交通 智慧交通系统

dapp链上合约质押挖矿系统开发详细流程/步骤逻辑/案例设计/源码模式

系统开发咨询1357O98O718

tidb-operator 安装 TiDB 集群

TiDB 社区干货传送门

集群管理 管理与运维 安装 & 部署 数据库架构设计 7.x 实践

一次莽撞的 TiDB 升级故障复盘

TiDB 社区干货传送门

版本升级

DR4019S|IPQ4019 IPQ4029 11AC SOM WIFI5

wallyslilly

IPQ4019

世界知识产权日:XSKY 以更多架构核心专利,推进 SDS 产业创新创造

XSKY星辰天合

星辰天合 世界知识产权日

浅谈Python在人工智能领域的应用

小魏写代码

BTC/ETH/IPFS/DAPP云算力质押模式挖矿分红系统开发详情介绍

系统开发咨询1357O98O718

答辩ppt要包含什么内容?分享2个制作答辩ppt的实用技巧!

彭宏豪95

PPT 大学生 在线白板 办公软件 演示文稿制作软件

TiDB性能优化-操作系统

TiDB 社区干货传送门

性能调优

合约跟单系统开发功能策略/需求设计/源码案例

系统开发咨询1357O98O718

javascript中symbol究竟是什么?

秃头小帅oi

ISO 专家解读 | 什么是 GQL 国际标准图查询语言

悦数图数据库

图数据库

聊天局:10年资深前端聊点行业现状

高端章鱼哥

如何构建更稳定高效的TiDB多租户系统

TiDB 社区干货传送门

新版本/特性解读 数据库架构设计 应用适配 HTAP 场景实践 7.x 实践

TiDB告警推送至企业微信机器人

TiDB 社区干货传送门

监控 集群管理

Operator 安装 TiDB 监控告警

TiDB 社区干货传送门

管理与运维 安装 & 部署 数据库架构选型 7.x 实践

阿里巴巴瓴羊基于 Flink 实时计算的优化和实践

Apache Flink

大数据 flink 实时计算

TiDB 在 CDC 同步下的主备切换

TiDB 社区干货传送门

集群管理 管理与运维 备份 & 恢复 6.x 实践 7.x 实践

测试 k8s 安装

TiDB 社区干货传送门

管理与运维 7.x 实践

突破数据存储瓶颈!转转业财系统亿级数据存储优化实践

TiDB 社区干货传送门

马斯克的 xAI 融资 60 亿美元;英伟达收购两家 AI 创企丨 RTE 开发者日报 Vol.193

声网

量化交易搬砖套利对冲系统开发指南详细/源码功能

系统开发咨询1357O98O718

万界星空科技MES系统在食品加工行业的应用

万界星空科技

制造业 mes 万界星空科技 食品行业 食品加工

QCN6274 vs QCA9880: Comparison of SOC and wireless communication chips

wifi6-yiyi

wifi qcn6274

Copilot的魔法让TiDB离线升级变得轻松愉快

TiDB 社区干货传送门

版本测评 8.x 实践

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