“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

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


公众号推荐:

2024 年 1 月,InfoQ 研究中心重磅发布《大语言模型综合能力测评报告 2024》,揭示了 10 个大模型在语义理解、文学创作、知识问答等领域的卓越表现。ChatGPT-4、文心一言等领先模型在编程、逻辑推理等方面展现出惊人的进步,预示着大模型将在 2024 年迎来更广泛的应用和创新。关注公众号「AI 前线」,回复「大模型报告」免费获取电子版研究报告。

AI 前线公众号
2020-10-20 10:521562
用户头像

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

关注

评论

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

记一次MHA切换故障踩的坑

一个有志气的DB

MySQL 高可用 复制 主从同步 故障分析

高仿瑞幸小程序 09 云数据库初体验

曾伟@喵先森

小程序 微信小程序 大前端 移动

游戏夜读 | Scikit-learn的2018自述

game1night

零基础如何学架构

兆熊

架构

服务发现:ZooKeeper vs etcd vs Consul

Tux Hu

Docker 容器 微服务 etcd Consul

看完这篇HTTP,跟面试官扯皮就没问题了

cxuan

https okhttp

如何在 Mac 上优雅的截图和录屏

Winann

macos 效率 效率工具 Mac

谈谈控制感(4):损失的后果很严重

史方远

职场 心理 成长

Java实现Base64

Java

elasticsearch源码解析(一)——restapi

罗琦

elasticsearch 源码分析 RESTful

为什么软件开发很难外包

刘华Kenneth

外包 DevOps 风险 背锅

找一个更好的理由

史方远

职场 成长 工作

初次见面

KAMI

Azure App 部署Django 和 PostgrSQL

yann [扬] :曹同学

Python azure

程序员的晚餐 | 5 月 15 日 如果不写代码了,那就开个饺子店

清远

美食

决战下半场:小程序技术助力金融APP重回C位

FinClip

小程序 数字化转型 app重构

游戏发行中学到的重要经验(严肃长文)

谢锐 | Frozen

独立开发者 游戏开发 游戏出海 移动互联网

“四个维度” 讲明白什么是微服务!

周果

微服务 单体系统 架构设计 团队组织 康威定律

谈谈双亲委派模型的第四次破坏-模块化

寻筝

Java JVM

JVM源码分析之Object.wait/notify实现

猿灯塔

JVM

Web3极客日报#140

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

阿里巴巴泰山版《Java 开发者手册》,也是一份防坑指南

古时的风筝

Java规范 Java开发手册

Leetcode 556. Next Greater Element III

隔壁小王

算法

回“疫”录(17):返宁的前一天

小天同学

疫情 回忆录 现实纪录 纪实

一篇文章搞定Java处理Excel的各种疑难杂症

知春秋

Java Excel POI

浅析 - CocoaLumberjack 3.6 之 DatabaseLogger

Edmond

ios sqlite log4j CocoaLumberjack DDLog

经历过疫情,你懂得了什么

Winann

疫情 个人成长 生活 成长

战略懒惰有多可怕

Neco.W

创业 重新理解创业 初创公司

奔腾吧,“后浪”李子柒!

无量靠谱

网红

数列找规律的问题

oldj

数学

网站系统架构演进

Janenesome

读书笔记 程序员 架构 系统设计

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