2. 服务端请求生命周期与SSRF注入点
要治理SSRF,先要看清一个服务端请求从“用户可控输入”到“真正发出网络连接”之间,究竟经过了哪些环节。SSRF的本质并不是某个函数有漏洞,而是用户可控的数据在某个环节被当作目标地址使用,且缺少足够的约束与校验。因此,定位注入点的关键,是把一次服务端请求拆成可观察的生命周期阶段,再逐段追问:这一段里,哪些值来自外部?它们是否被校验?校验是否可被绕过?
典型的一次服务端出站请求,会经历如下阶段:业务入口接收参数 → URL/地址拼接与解析 → 协议与主机判定 → DNS解析 → 建立TCP连接 → 发送HTTP请求 → 接收并处理响应。SSRF注入点可能出现在其中任意一段,而且往往不是单点,而是“入口宽松 + 解析歧义 + 校验与使用不一致”的组合。
一、业务入口:参数直连是最常见的注入点
很多SSRF的起点非常朴素:接口接收一个URL参数,然后直接交给HTTP客户端发起请求。例如图片抓取、Webhook回调、URL预览、远程文件下载、PDF渲染、数据导入等场景,都会出现类似 url、target、callback、imageUrl 这样的参数。如果这个参数没有经过任何限制,攻击者就可以把它替换为 http://127.0.0.1:6379、http://169.254.169.254/latest/meta-data/ 或内网管理地址。
这一层的防御重点不是“过滤关键字”,而是明确业务到底需要访问哪些目标。如果业务只需要访问固定的几个域名,就应该使用白名单,而不是黑名单。白名单的粒度可以是完整域名、域名后缀或IP段,但必须与业务实际需求对齐。
二、URL解析:歧义与不一致是绕过校验的温床
URL解析阶段是SSRF防御中最容易出错的地方。不同语言、不同库对同一个URL字符串的解析结果可能不同。例如 http://evil.com@127.0.0.1/,有的解析器把 evil.com 当作用户名、127.0.0.1 当作主机;有的则可能把整个字符串当作主机。再比如 http://127.0.0.1#@evil.com、http://127.0.0.1%2f@evil.com、十进制IP http://2130706433/、八进制 http://0177.0.0.1/ 等写法,都可能让校验逻辑与实际连接目标不一致。
正确的做法是:先解析、再校验、后使用,并且校验和连接必须使用同一个解析结果。不要用字符串匹配去判断主机,而应取出解析后的host字段,再对其做规范化处理(如转小写、去除末尾点、还原编码),然后判断是否在白名单内。
三、DNS解析:解析与连接分离带来的重绑定风险
即使校验时域名解析到的是公网IP,真正建立连接时DNS可能返回另一个内网IP,这就是DNS重绑定。防御方式有两种:一是解析一次,把IP固定下来,后续连接直接使用该IP,并在Host头中保留原域名;二是在连接层再次校验目标IP,确保实际连接的地址仍然合法。两者结合更稳妥。
四、连接建立与响应处理:重定向与回显是最后两道口子
很多HTTP客户端默认跟随重定向。攻击者可以提供一个返回302的合法公网地址,重定向到内网目标。因此,要么禁止跟随重定向,要么在每一跳都重新执行完整校验。响应处理阶段同样关键:如果服务端把目标返回的内容、状态码、响应时间甚至错误信息原样返回给用户,攻击者就能利用这些差异探测内网端口开放情况。防御上应最小化回显,只返回业务必需的信息,并对错误信息做统一处理。
五、架构层面的注入点:不止在代码里
SSRF也可能出现在架构层。例如反向代理、API网关、消息队列消费者、定时任务、CI/CD流水线、Serverless函数等,只要它们会代表服务端发起出站请求,就可能成为跳板。尤其是微服务架构中,服务间调用如果信任了上游传入的URL,风险会沿着调用链放大。因此,治理SSRF不能只盯着单个接口,还要梳理所有出站请求的发起方,建立统一的出站策略。
| 生命周期阶段 | 典型注入点 | 核心防御动作 |
|---|---|---|
| 业务入口 | url参数直连HTTP客户端 | 白名单限制目标域名/IP段 |
| URL解析 | 解析歧义、编码绕过 | 统一解析器,校验与连接使用同一结果 |
| 校验判定 | 黑名单不全、正则缺陷 | 改为白名单,规范化后判断 |
| DNS解析 | DNS重绑定 | 固定解析IP,连接层二次校验 |
| 连接建立 | 重定向跟随、协议滥用 | 禁止重定向或逐跳校验,限制协议 |
| 响应处理 | 响应回显、错误信息泄露 | 最小化回显,统一错误处理 |
把这张生命周期图记在脑子里,后续无论是做代码审计、渗透测试还是安全设计,都可以按阶段逐项检查。SSRF的治理不是加一个正则就结束,而是要在每个阶段都保证“用户可控的数据不会未经校验地变成网络连接的目标”。