写点什么

如何保护你的 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:521676
用户头像

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

关注

评论

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

动手实践开发一个智慧路灯控制器

华为云开发者联盟

后端 物联网 华为云 华为云开发者联盟 企业号 3 月 PK 榜

快速开始高性能Elasticsearch客户端bboss

大河

elasticsearch java bboss restclient

用这三本书,探究 ChatGPT 的底层逻辑

图灵社区

深度学习 GPT #人工智能 ChatGPT

流量调度、微服务可寻址性和注册中心

有态度的马甲

Dragonfly 最新版本 v2.0.9 发布

SOFAStack

开源 互联网 开发者 开发

Web前端设计开发工具集(JS框架、CSS预处理)

2D3D前端可视化开发

前端开发 代码编辑器 css预处理器 web前端开发 前端开发工具

推荐一个比jmeter更轻量的开源测试平台:RunnerGo

爱研究代码的极客人

Jmeter 性能测试 自动化测试 压力测试 LoadRunner

信息抓包工具:Charles 激活版

真大的脸盆

Mac Mac 软件 抓包工具 信息抓包

2023年春招Java面试刷题小抄,从P5~P8全家桶教学,全部刷完大厂Offer拿到手软

采菊东篱下

Java 面试

Apache HugeGraph1.0.0 版本正式发布!

百度安全

4.0 功能抢先看 | 读懂一个项目的研发效能 之 项目交付效率

思码逸研发效能

研发效能

修复SSH在 MacOS Ventura 系统上不能使用RSA签名的问题

互联网搬砖工作者

多平台小程序一站式管理分享

FinClip

2023年,LED显示屏配套设备急需升级和优化

Dylan

产品 制造 LED显示屏

分享:如何给 DBA 减负?

OceanBase 数据库

数据库 oceanbase

实用性好的云管平台有哪些?咨询电话多少?

行云管家

云计算 云资源 云管理

ChatGPT4 给出数据库开发者最容易犯的10个错误和解决方案

NineData

数据库 程序员 开发者 dba ChatGPT

用这三本书,探究 ChatGPT 的底层逻辑

图灵教育

深度学习 GPT #人工智能 ChatGPT

Docker等容器技术如何与移动开发相结合

FinClip

AutoCAD安装失败,提示错误“Error 112”和安装进度条倒退为0

互联网搬砖工作者

从DPU角度,谈谈关于国产OS开源社区发展的思考

大禹智芯

DPU 国产OS开源社区

喜讯!华秋电子荣登“2022年中国产业互联网百强企业”榜单

华秋电子

文本数据标注,支持词典导入及更多快捷方式|ModelWhale 版本更新

ModelWhale

机器学习 数据分析 云平台 标注 标注工具

爱因斯坦霉霉同框只需15秒,最新可控AI一玩停不下来,在线试玩已出丨开源

Openlab_cosmoplat

开源社区 AI绘画

分享:FactorJoin,一种新的连接查询基数估计框架

OceanBase 数据库

数据库 oceanbase

选择KV数据库最重要的是什么

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 3 月 PK 榜

数据采集&流批一体化处理使用指南

大河

批处理 ETL 流处理 bboss 流批一体化

直播指南!解锁 OceanBase DevCon • 2023

OceanBase 数据库

数据库 oceanbase

photoshop 2023存储为窗口显示空白、黑屏如何解决

互联网搬砖工作者

用138个案例讲明白了Spring全家桶+Docker+MQ

Java你猿哥

spring 面试 Spring Cloud Spring Boot 面经

HUAWEI Mate X3带来全新小艺输入法, 9键双键盘左右开工、语音悬浮气泡免干扰

最新动态

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