graphql-core:十万行注释,为什么一条都没算进 max\_tokens
原创
云梦DC
云梦DC
云梦安全
2026年10月29日 11:21
河南
去阅读
2026 年 10 月 22 日,graphql-core 披露了 CVE-2026-75508(GHSA-r77w-qph3-7vf8,CVSS 7.5),3.2.12 和 3.3.0rc1 修复。问题卡在 max_tokens 的计数口径上:它限制的是词法分析器交给语法层的有效 token,注释虽然被跳过,却照样创建对象、挂在链表上。
本地构造的一份查询大约 390 KB,真正的语法 token 只有 5 个,中间塞了 10 万条 # c 注释。3.2.8 在 max_tokens=5 下接受了它,解析完成后,loc 链表里有 100005 个 Token 对象。
漏洞速览
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2026-75508 / GHSA-r77w-qph3-7vf8 |
| 影响组件 | graphql-core 3.2.3–3.2.11、3.3.0a3–3.3.0beta |
| 修复版本 | 3.2.12、3.3.0rc1 |
| 漏洞类型 | 资源耗尽(CWE-400/770) |
| 危害等级 | CVSS 7.5 High |
| 触发前提 | 服务端调用 parse() 时启用了 max_tokens |
注释没有参与语法,但参与了分配
GraphQL 词法分析器在推进到下一个有效 token 时,会跳过空白和注释。调用方看到的是 advance_lexer() 返回了一段有效内容,max_tokens 的计数也跟着增加;注释不返回 token,自然没有计数。
解析器同时还要保留位置信息,方便报错时给出行列号。为此,扫描到的词法单元都会变成 Token 对象,通过 prev / next 组成双向链表,挂在文档的 loc 上。注释也走这条路径:先分配、记录位置、接进链表,然后才被跳过。
结果就是两个数字对不上。max_tokens 数的是语法层拿到的 token,内存里留下的是扫描过的全部 token。攻击者只要把大量内容写成注释,配额看到的仍然是一份很小的查询。
四组输入的对照
实验把 3.2.8 和 3.2.12 装进两个目录,用同一份脚本跑四组输入。
第一组是 { a },max_tokens=5,两版都通过,链表里是 5 个 token。第二组放 6 个真实 token,两版都抛 GraphQLSyntaxError。配额对普通 token 是有效的,这也说明为什么只做常规查询测试不容易碰到这个问题。
第三组是核心用例:10 万条注释后接 { a },max_tokens=5。3.2.8 接受,耗时 172 毫秒,顺着 loc 走链表可以数出 100005 个 Token。3.2.12 在相同输入上抛错,耗时 177 毫秒,拒绝发生在配额检查阶段。
第四组把注释加到 20 万条,源文本 9.7 MB。3.2.8 仍然接受,tracemalloc 峰值 55.3 MB;3.2.12 拒绝。这里要注意,修复版拒绝时也会先扫描并分配一部分 token,所以它的峰值不一定更低。修复解决的是“上限能不能生效”,不是“拒绝过程不花内存”。
深度限制和白名单为什么帮不上
深度限制、复杂度分析和查询白名单都要先拿到 AST,而 AST 是 parse() 成功之后才产生的。等这些规则开始工作,10 万个注释 token 已经分配完。
请求体大小限制理论上更靠前,但这类内容的压缩率很高。重复的 # c\ 被打包后可能只有几十 KB。如果大小检查放在反向代理上,看的又是压缩后的请求体,它看到的会是一份很普通的请求;等框架解压完再交给解析器,体积才展开。
所以缺口落在“解压之后、解析之前”。这个位置需要在应用侧补,单靠 GraphQL 的查询规则补不上。
升级后哪些请求会变
3.2.12 调整了 advance_lexer() 的计数,把两次有效 token 之间跳过的注释也算进去,并且覆盖到 EOF 前的最后一批注释。这样 max_tokens 约束的就不只是语法 token,而是解析器实际创建的 Token。
副作用是注释开始占配额。带大量文档注释的 SDL、由工具生成且保留注释的 schema,以及客户端里写长段说明的查询,升级后都可能被拒。公告建议这类场景调高 max_tokens,解析结果里的 token_count 口径也跟着变了。
上线前最好先从生产流量采样,算一下正常文档的注释占比,再按新口径重设上限。灰度时重点看 GraphQLSyntaxError 的比例;如果某类客户端被误伤,优先调整配额或让客户端去掉注释,不建议为了兼容直接回退版本。
还需要补的边界
解压后的请求体要有独立上限,不能只依赖 max_tokens。这个值应明显低于进程内存上限,解压过程也要放在有边界的缓冲区里。
解析阶段可以单独设超时和内存预算。它是纯 CPU 和内存操作,不依赖下游服务,适合放进隔离的执行单元;超时后直接丢弃,避免拖住整个 worker。容器内存也别卡得太紧,公告提到 256 MB 或 512 MB 的容器在几个并发请求下就可能触发 OOM。
日志里可以统计注释字符与有效字符的比例,以及请求体解压前后的体积差。正常业务查询的注释占比通常不高,几十 KB 解压成几 MB 的请求值得单独记录。解析耗时和请求体大小明显背离时,也值得回看原始文本。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论