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