作者 akihi(白帽攻防录讲师),某甲方网络安全工程师,合合 SRC 年度第一、腾讯 SRC 连续三年前十,单漏洞赏金 4w+,擅长 Web/App/PC 客户端漏洞挖掘。专注 GraphQL 安全、数据库注入、NoSQL 安全。本文基于公开 CVE 信息与技术原理深度复盘,供安全研究与防护参考。
一、漏洞时间线
2026 年 4 月,开源分布式 GraphQL 数据库 Dgraph 被曝出一个严重的未授权访问漏洞 CVE-2026-41328。在默认配置下(未启用 ACL),攻击者只需要发送两个 HTTP 请求,就能读取数据库里的所有数据------相当于把整个数据库裸奔在公网上。
| 时间 | 事件 | 影响版本 |
|---|---|---|
| 2026-04-15 | 相关 Dgraph 漏洞陆续披露 | Dgraph 25.3.x 之前 |
| 2026-04-24 | GHSA-x92x-px7w-4gx4 发布 | ------ |
| 2026-04-24 | NVD 收录 CVE-2026-41328 | ------ |
| 2026-04-29 | 安全厂商发布技术分析 | ------ |
| 修复版本 | Dgraph 25.3.3 | ------ |
这个漏洞最可怕的地方在于:利用条件极低------不需要登录,不需要任何凭据,只要数据库端口暴露在公网上,发送两个 POST 请求就能拖走整个数据库。而 Dgraph 的默认配置恰恰就是不启用 ACL 的,很多部署直接用默认配置就上线了。
二、攻击链全景
让我们先用一张流程图还原完整的攻击路径:
攻击者发现暴露在公网的 Dgraph 数据库(端口 8080)
│
▼
第一步:确认目标配置
│ Dgraph 运行在默认配置
│ 没有启用 ACL 认证
│ /alter 接口可以未授权访问
│ /mutate 接口也可以未授权访问
▼
第二步:发送第一个请求(创建 schema)
│ POST /alter HTTP/1.1
│ Content-Type: application/json
│ {"schema": "email: string @unique @index(exact) @lang ."}
│ 这个请求创建了一个特殊的 predicate:
│ - @unique 唯一约束
│ - @index(exact) 精确索引
│ - @lang 多语言支持
│ 为什么要加 @lang?
│ 因为 @lang 会触发 Lang 字段的解析逻辑
│ 这就是注入点!
▼
第三步:发送第二个请求(注入 DQL)
│ POST /mutate?commitNow=true HTTP/1.1
│ Content-Type: application/json
│ {
│ "set": {
│ "email@') } } { injection(func: has(password)) { uid password } # ": "test@test.com"
│ }
│ }
│ 注意这个 key:
│ Dgraph 解析这个 key:
│ - predicate = "email"
│ - Lang = "') } } { injection(func: has(password)) { uid password } #"
│ Lang 字段被拼接到 DQL 查询里!
│ 注入成功!
▼
第四步:服务器执行注入查询
│ Dgraph 的 addQueryIfUnique 函数
│ 用 fmt.Sprintf 拼接 DQL 查询
│ Lang 字段没有做任何转义
│ 拼接出来的查询里
│ 攻击者注入的代码:
│ - ') } } 闭合 eq() 函数和查询块
│ - { injection(...) } 新增一个查询块
│ - # 注释掉后面的模板代码
▼
第五步:数据库返回所有数据
│ 注入的查询执行了
│ func: has(password)
│ 意思是:找到所有有 password 字段的节点
│ 返回 uid 和 password
│ 攻击者拿到了所有用户的密码!
│ 整个数据库的数据都被读走了
▼
攻击者成功拖走整个数据库
│
├─ 读取所有用户数据
├─ 窃取密码、令牌等敏感信息
├─ 完全控制数据库
└─ 数据泄露影响整个业务
**关键结论:**这个漏洞的本质是"参数拼接成代码"------Lang 字段本来只是个语言标签(比如 "en"、"zh"),但开发者把它直接拼接到 DQL 查询里,没有做任何转义。攻击者把 Lang 字段变成了注入代码,闭合了原来的函数,插入了自己的查询。这就是经典的注入漏洞------外部输入被当成代码执行了。
三、Dgraph 与 GraphQL 原理
3.1 什么是 Dgraph
Dgraph 是一个开源的分布式 GraphQL 数据库:
| 组件 | 功能 |
|---|---|
| Dgraph Alpha | 主节点,处理查询和变更 |
| Dgraph Zero | 集群管理、分片分配 |
| HTTP 端口 8080 | REST API 接口 |
| gRPC 端口 9080 | RPC 接口 |
| /alter | 修改 schema |
| /mutate | 插入/更新数据 |
| /query | 查询数据 |
3.2 什么是 DQL
DQL(Dgraph Query Language)是 Dgraph 的查询语言,基于 GraphQL:
// 正常的 DQL 查询
query {
user(func: eq(name, "Alice")) {
name
email
age
}
}
// 这个查询的意思是:
// 找到 name 等于 "Alice" 的用户
// 返回他们的 name、email、age
3.3 什么是 Predicate 和 Lang
在 Dgraph 中:
- Predicate:属性,比如 name、email、password
- Lang:多语言标签,支持同一个属性有不同语言的值
- 格式:predicate@lang,比如 name@en、name@zh
- 解析时按 @ 分割成 predicate 和 lang 两部分
四、漏洞深度分析
4.1 漏洞根因:字符串拼接
根据 OSV 和安全分析,漏洞的根本原因是 addQueryIfUnique 函数用 fmt.Sprintf 拼接 DQL 查询:
// 有缺陷的代码(伪代码)
func addQueryIfUnique(predicate string, lang string, value string) string {
// 用 fmt.Sprintf 拼接查询
query := fmt.Sprintf(
"query { var(func: eq(%s, \"%s\")) { uid %s@%s } }",
predicate, value, predicate, lang,
)
return query
}
// 问题在哪里?
// lang 参数直接拼接到查询里
// 没有做任何转义或验证
// 攻击者控制了 lang 参数
// 就能注入任意 DQL 代码
4.2 注入点在哪里
注入点是 JSON mutation 的 key:
// 正常的 mutation
{
"set": {
"name@en": "Alice"
}
}
// Dgraph 解析:
// key = "name@en"
// predicate = "name"
// lang = "en"
// value = "Alice"
// 攻击者的注入
// key 里包含注入代码
// Dgraph 按 @ 分割
// predicate = "email"
// lang = 注入的 DQL 代码
// lang 被拼接到查询里
// 注入成功!
4.3 注入 payload 拆解
让我们拆解一下这个注入 payload:
// 注入的 lang 值:
// ') } } { injection(func: has(password)) { uid password } #
// 逐部分分析:
// 1. ' ------ 闭合字符串
// 2. ) ------ 闭合 eq() 函数
// 3. } ------ 闭合第一个查询块
// 4. } ------ 闭合整个查询
// 5. { injection(func: has(password)) { uid password } } ------ 新增查询块
// 6. # ------ 注释掉后面的模板代码
// 服务器解析后变成两个查询:
// 第一个是正常的 unique 检查
// 第二个是攻击者注入的查询
// 第二个查询返回所有有 password 字段的数据
4.4 影响范围
| 项目 | 详情 |
|---|---|
| CVE 编号 | CVE-2026-41328 |
| 漏洞类型 | DQL 注入(CWE-943) |
| 影响产品 | Dgraph 分布式 GraphQL 数据库 |
| 影响版本 | 25.3.3 之前 |
| 利用条件 | 默认配置(未启用 ACL) |
| 认证要求 | 无需认证 |
| 利用难度 | 低,两个 HTTP POST 请求 |
| 影响 | 读取整个数据库的所有数据 |

五、修复方案分析
Dgraph 在 25.3.3 版本中修复了这个问题:
| 修复项 | 修复方式 |
|---|---|
| Lang 字段验证 | 严格验证 Lang 字段格式,只允许合法语言标签 |
| 参数化查询 | 用参数化查询替代字符串拼接 |
| 输入转义 | 对所有用户输入做转义处理 |
5.1 注入漏洞防护原则
// 注入漏洞的根本原因
// 外部输入被当成代码执行
// 防护原则:
// 1. 永远不要拼接 SQL/DQL/Shell 命令
// 2. 用参数化查询
// 3. 对输入做严格验证
// 4. 最小权限原则
// 不好的做法:
// 用 fmt.Sprintf 拼接查询字符串
// 好的做法:
// 用参数化查询,参数和代码分开
5.2 数据库安全最佳实践
// 数据库安全检查清单
// 1. 不要把数据库暴露在公网
// 数据库只应该监听内网地址
// 不要直接暴露 8080、9080 端口
// 2. 启用认证和授权
// 不要用默认配置
// 启用 ACL,设置强密码
// 3. 最小权限
// 应用程序用的数据库账号
// 只给必要的权限
// 不要用 root/admin 账号
// 4. 输入验证
// 所有用户输入都要验证
// 不要信任任何客户端输入
// 5. 定期审计
// 检查是否有注入漏洞
// 检查配置是否安全
六、SRC 审计启示录
6.1 GraphQL/NoSQL 安全审计清单
在 SRC 挖洞过程中,针对 GraphQL 和 NoSQL 数据库的审计清单:
| # | 检查项 | 检测方法 |
|---|---|---|
| 1 | 是否有注入漏洞 | 测试各种特殊字符和语法 |
| 2 | 是否暴露调试接口 | 访问 /graphql/console、/debug 等 |
| 3 | 是否启用认证 | 直接访问 API 看要不要登录 |
| 4 | 是否有过度查询 | 测试深层嵌套查询 |
| 5 | 是否泄露 schema | 发送 introspection 查询 |
6.2 常见注入漏洞类型
// 常见的注入漏洞
// 1. SQL 注入
// 关系型数据库
// 经典的 ' OR 1=1--
// 2. NoSQL 注入
// MongoDB、Cassandra 等
// 利用操作符($where、$gt 等)
// 3. GraphQL 注入
// 就是本次漏洞的类型
// 注入查询语言
// 4. 命令注入
// 系统命令
// ; ls -la
// 5. LDAP 注入
// LDAP 查询注入
// 6. XPath 注入
// XML 查询注入
6.3 数据库暴露面分级
| 暴露级别 | 情况 | 风险 |
|---|---|---|
| 严重 | 数据库直接暴露公网,无认证 | 任何人都能访问 |
| 高 | 数据库暴露公网,有弱密码 | 容易被破解 |
| 中 | 内网暴露,未做访问控制 | 横向移动可达 |
| 低 | 内网访问,有认证和授权 | 相对安全 |
七、防护建议
- 及时更新:升级 Dgraph 到 25.3.3 以上版本
- 启用 ACL:不要用默认配置,启用认证授权
- 限制访问:不要把数据库端口暴露在公网
- 输入验证:严格验证所有用户输入,特别是 Lang 等特殊字段
- 参数化查询:用参数化查询替代字符串拼接
- 定期审计:测试各种注入漏洞和配置错误
八、总结
CVE-2026-41328 是一个典型的"注入漏洞"------它提醒我们:只要是把外部输入拼接到代码里,就有注入风险。不管是 SQL、DQL、Shell 命令还是别的什么查询语言,原理都是一样的。在 SRC 挖洞实践中,数据库是高价值目标:一旦拿到数据库权限,整个业务的数据就都泄露了。测试各种特殊格式的输入、探索隐藏的 API 接口,往往能发现默认配置下的严重漏洞。
