## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-0XMRMA-CVE-2026-34207
# CVE-2026-34207
SSRF过滤器检查了主机名字符串,但实际目的地是由后续的DNS决定的。这一漏洞使得攻击者控制的Webhook URL能够到达回环地址、元数据服务和私有网络目标。
## 介绍
我在审查**Typebot** (一个开源聊天机器人构建器)时发现了这个漏洞,心中带着一个简单的安全问题:
**如果SSRF保护验证的是主机名字符串,而实际目的地是由后续的DNS决定的,会发生什么?**
在这个案例中,这个问题引出了一个真正的漏洞。
Typebot 的 `Webhook` / HTTP 请求块的 SSRF 保护仅验证了:
* URL 字符串,
* 封禁了主机名字面量,
* 以及字面量 IP 格式
它**没有** 在允许请求之前解析主机名。
这意味着像 `ssrf-repro.example` 这样的主机名在验证时看起来无害,但随后可能解析为:
* `127.0.0.1`
* `169.254.169.254`
* 或 RFC1918/私有网络空间
并且仍然会被后端 HTTP 客户端获取。
这个问题成为了 **CVE-2026-34207** 。
**Typebot:** Typebot on GitHub
**CVE:** CVE-2026-34207
**修复版本:** `3.16.0`
这影响了**Typebot** ,一个被广泛使用的开源聊天机器人平台。在其官方网站上,Typebot 被宣传为受全球 **650+ 家公司** 信赖。该网站还宣称拥有 **200 万+ 月聊天量** 和 **150 万+ 已发布的机器人** 。

* * *
## 攻击链
`攻击者控制的 Webhook URL -> 主机名通过仅验证字面量的 SSRF 检查 -> 在允许判定前未进行 DNS 解析 -> 后端 HTTP 客户端将主机名解析为内部目标 -> 服务端请求到达回环地址 / 元数据服务 / 私有网络 -> 响应数据通过执行日志变得可用`
* * *
## Typebot 的功能
**Typebot** 是一个聊天机器人构建器。
它允许用户创建可以执行以下操作的流程:
* 提问
* 收集结构化输入
* 调用外部服务
* 串联业务逻辑
* 并通过 `Webhook` / HTTP 请求块触发出站 HTTP 请求
这意味着出站请求执行是一个真正的安全边界。
这里的关键问题不是 Typebot 是否支持 Webhook 块。
真正的问题是:
**SSRF 保护验证的是服务器将要连接的实际目的地,还是只是 URL 中出现的主机名字符串?**
在这个案例中,它首先只验证了文本形式。
这就是错误所在。
* * *
## 为什么这个攻击面值得关注
SSRF 防御以非常可预测的方式失效。
大多数时候,有趣的错误不是:
* "你忘了封禁 `169.254.169.254`"
* 或者 "你忘了封禁 `localhost`"
更强的错误是边界错误:
* 验证发生在规范化之前
* 验证发生在重定向之前
* 验证发生在 DNS 解析之前
* 验证发生在一个表示上,但网络栈使用了另一个
这正是需要检查的地方。
Typebot 已经拥有针对字面量元数据 IP、回环、私有范围和编码 IP 技巧的 SSRF 强化逻辑。
这使得下一个问题显而易见:
> **如果主机名本身不是危险的,但后来解析到了一个危险的目的地,会发生什么?**
这正是发生的情况。
* * *
## 根本原因
根本问题是**基于主机名字符串而非已解析 IP 地址的目的地验证** 。
在易受攻击的实现中,`validateHttpReqUrl()`:
* 解析了 URL
* 只允许 `http:` 和 `https:`
* 封禁了一小部分字面量主机名,例如 `metadata.google.internal`、`metadata.goog`、`metadata` 和 `localhost`
* 检测字面量十进制 / 十六进制 / 八进制 IP 技巧
* 解析字面量 IPv4 / IPv6 地址
* 仅当主机名本身已经是字面量 IP 时才验证该地址
这是关键部分。
如果主机名是一个普通值,例如:
`ssrf-repro.example`
那么 `parseIPAddress(hostname)` 返回 `null`,验证器就此停止。
在批准之前没有进行 DNS 解析。
因此,脆弱的逻辑实际上简化为:
root@kitploit:~
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
这意味着:
* 字面量危险 IP 被封禁
* 编码的危险 IP 被封禁
* 但解析为危险 IP 的主机名未被封禁
漏洞的另一半在执行路径中。
在 `executeHttpRequest()` 中,Typebot 首先运行验证,然后稍后使用 `ky(request.url, ...)` 执行实际请求。
因此,顺序是:
* 验证 URL 字符串
* 接受主机名
* 稍后在真实出站请求期间解析主机名
* 连接到已解析的内部目标
这就是完整的漏洞。
* * *
## 为什么这是一个安全问题,而不仅仅是不完整的过滤
重要的区别在于**信任决策是在哪里做出的** 。
许多漏洞如果描述不当,看起来都很小。
如果你把这个漏洞描述为:
> “主机名过滤器不完整”
听起来像是一个质量问题。
但这并不是真正的问题。
真正的问题是:
* 服务器在知道实际目的地之前就做出了安全决策
* 网络栈稍后连接到了其他地方
* 并且应用程序将该请求视为有效
这不是表面的过滤弱点。
这是一个信任边界失败。
而且由于 HTTP 执行器在执行日志中记录了响应数据,这个问题在最严重的情况下甚至不是盲目的。
所以这不只是:
* “发生了意外的内部流量”
而是:
* 内部流量发生了
* 并且攻击者通常可以通过正常应用程序行为恢复证明和响应内容
这是一个真正的 SSRF 漏洞。
* * *
## 概念验证
我使用了两个验证层,因为它们展示了两个不同方面。
### PoC 1:自包含本地解析器演示
第一个 PoC 清晰地隔离了根本原因。
我使用了一个小型本地测试框架,该框架:
* 在 `127.0.0.1` 上启动了一个回环 HTTP 服务器
* 验证了诸如 `http://ssrf-repro.example:18080/...` 这样的 URL
* 使用受控解析器,使得 `ssrf-repro.example` 解析为 `127.0.0.1`
* 然后执行请求
这精确演示了缺陷:
* 验证通过,因为主机名不是字面量封禁值
* 后续请求仍然到达了回环地址
捕获的输出显示:
* 验证结果:通过
* 请求执行结果:到达回环
* 从回环服务返回的响应体
这直接证明了验证的缺口。
* * *
### PoC 2:真实的 Typebot 执行路径
第二个 PoC 通过实际的功能路径展示了漏洞,这才是关键。
最简单的复现方法是:
1. 在 Typebot 后端解析 DNS 的机器上,将一个良性主机名映射到回环地址
2. 启动一个本地 HTTP 服务
3. 创建一个指向该良性主机名的 `Webhook` 块
4. 通过正常的经过身份验证的预览或实时执行路径触发该块
一个代表性的块如下所示:
root@kitploit:~
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
配合如下的 hosts 文件条目:
root@kitploit:~
127.0.0.1 ssrf-repro.example
验证器仍然接受了该 URL,因为:
* `ssrf-repro.example` 不在 `blockedHostnames` 中
* 它不是 `localhost`
* `parseIPAddress("ssrf-repro.example")` 返回 `null`
* 没有进行目的地 IP 验证
然后后端 HTTP 客户端将主机名解析为 `127.0.0.1` 并仍然连接了。
这确立了完整的论断:
* 易受攻击的验证逻辑在实际功能使用中是可达的
* 请求命中了被禁止的目的地类别
* 并且应用程序仍然将其视为一次成功的出站 HTTP 请求
* * *
## 为什么选择这两个 PoC
第一个 PoC 证明了根本原因。
第二个 PoC 证明了产品影响。
这种区分很重要。
如果你只展示:
> “这个过滤器接受了一个主机名”
你展示得还不够。
如果你只展示:
> “发生了一次内部请求”
你没有隔离原因。
更全面的报告是:
* 基于主机名的验证接受了一个本不应被信任的值
* 后续的 DNS 解析改变了该值的实际安全含义
* 后端连接到了一个被禁止的目的地
* 并且功能路径仍然完成
这是完整的故事。
* * *
## 严重性与分类
该问题被合理分类为**高** 。
公告分类为:
* **CWE-918** : 服务器端请求伪造 (SSRF)
* **CWE-20** : 输入验证不当
* **CVSS:**
root@kitploit:~
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
这说得通。
这本身不是一个未经身份验证的互联网范围的 SSRF。
所需的权限为**低** ,因为独立漏洞需要一个能够配置或触发 `Webhook` / HTTP 请求块的参与者。
但是在这个边界内,影响是严重的:
* 回环访问
* 私有网络访问
* 元数据访问
* 以及通过日志的数据泄露
这本身就是一个强 SSRF 问题。
而且当与其他扩大可达性的漏洞结合时,它变得更加危险。
* * *
## 为什么这仍然值得报告
有些人看到经过身份验证的 SSRF 就立即低估它。
这是一个错误。
真正的问题不是:
> “攻击者是否已登录?”
真正的问题是:
> “那个攻击者能否让服务器连接到安全模型本应禁止的地方?”
在这里,答案是肯定的。
验证器声称保护:
* 元数据服务
* 回环地址
* RFC1918 私有范围
* IPv6 本地范围
但一个主机名解析缺口让这些相同目的地通过另一种表示重新回来。
这正是那种值得获得 CVE 的漏洞。
* * *
## 修复分析
修复是扎实的,因为它纠正了真正的边界,而不仅仅是症状。
在修补后的实现中,`validateHttpReqUrl()` 被修改为在批准请求之前解析主机名。
验证器现在:
* 从 `node:dns/promises` 导入 `lookup`
* 将验证视为异步
* 在允许之前解析非字面量主机名
* 解析每个解析后的地址
* 对每个解析后的地址针对相同的封禁范围进行验证
这是正确的修复,因为它将信任决策从:
* “主机名字符串看起来可以吗?”
改为:
* “实际目的地是否解析为一个允许的地址?”
这是从一开始就应该存在的安全属性。
修复还增加了针对以下方面的回归测试:
* 解析为回环的主机名
* 解析为 RFC1918 空间的主机名
* 为自托管用户提供的允许列表内部主机
* 保留对元数据和回环目标的不可绕过保护
这是你希望在真实 SSRF 修复中看到的强化方式:
* 边界被纠正
* 预期行为在代码中有文档说明
* 并且测试锁定了该漏洞类别
该问题已在 **Typebot 3.16.0** 中解决。
* * *
## 披露
该问题通过 GitHub 的安全报告流程私下报告。
报告内容包括:
* `validateHttpReqUrl()` 中的根本原因
* `executeHttpRequest()` 中的下游执行路径
* 使用主机名解析到回环 / 私有目标的可复现策略
* 以及一个自包含的演示,用于隔离验证缺口
维护人员接受了该问题,修复了验证逻辑,该漏洞后来被发布为:
**CVE-2026-34207**
修复版本已发布在 **Typebot 3.16.0** 中。
* * *
## 这个漏洞实际上教会了我们什么
关键教训很简单:
> 安全决策必须基于网络栈实际使用的相同表示。
这听起来很明显。
但许多 SSRF 保护之所以失效,正是因为没有遵循这条规则。
* 一个主机名字符串看起来安全。
* DNS 改变了它的含义。
* 请求仍然发出去了。
这就够了。
这个漏洞还强化了关于 SSRF 审查的重要一点:
* 字面量 IP 过滤是不够的
* 主机名黑名单是不够的
* 编码 IP 检查是不够的
如果你不解析并验证实际目的地,你的 SSRF 过滤器仍然是不完整的。
这才是真正的收获。
* * *
## 关键要点
* 当验证发生在目的地解析之前时,SSRF 防御会失效
* 主机名字符串验证并不等同于目的地 IP 验证
* Webhook / HTTP 请求功能是真正的安全边界
* 经过身份验证的 SSRF 在到达元数据和内部服务时仍然可以是高严重性
* 一份强有力的报告要连接根本原因和影响,而不仅仅是其中之一
* 修复是正确的,因为它将决策移到了实际解析的目的地
* * *
## 最后的话
这个漏洞并不在于什么花哨的 payload。
而在于提出正确的边界问题。
在 Typebot 中,SSRF 验证器首先检查了主机名字符串。 实际目的地是由后续的 DNS 决定的。 网络客户端遵循了解析后的地址。
这个缺口就是漏洞。
这就是为什么它成为了 **CVE-2026-34207** 。
已在 **Typebot 3.16.0** 中修复。