背景
SSRF(Server-Side Request Forgery)在实际业务里并不罕见,尤其是带有“URL 预览、远程图片抓取、Webhook 回调、文件转存、第三方资源导入”等功能的系统。它的危险点不只在于服务端可以代替用户发起请求,更在于服务端通常位于内网或云环境中,能够访问普通用户无法触达的地址。
这篇文章整理一次比较典型的 SSRF 排查与修复过程,重点放在识别思路、代码审计关注点、可验证的安全测试方式和工程化防护落地。文中示例均以本地靶场和授权测试环境为前提,不涉及真实目标的攻击利用。
问题场景
某业务提供“远程图片导入”能力,用户提交一个图片 URL,后端下载图片并保存到对象存储。初始实现逻辑大致如下:
POST /api/image/import
Content-Type: application/json
{
"url": "
}从功能角度看,这类接口很常见:校验 URL、下载文件、判断类型、保存。但在安全审计中,需要重点确认以下问题:
- 是否允许访问内网地址、本机地址、云元数据地址;
- 是否跟随 30x 跳转,跳转后是否重新校验目标地址;
- 是否仅依赖字符串黑名单判断;
- 是否限制协议类型,例如 file、gopher、ftp 等;
- 是否存在 DNS Rebinding 风险;
- 是否设置超时、响应大小限制,避免被拖垮;
- 是否将错误信息原样返回,导致内网探测信息泄露。
技术分析
SSRF 的根源通常不是“有一个能请求 URL 的功能”,而是“服务端把用户输入的 URL 当成可信目标直接请求”。下面从几个容易被忽略的点展开。
1. URL 解析不能只靠字符串判断
一些代码会用 startsWith 或 contains 判断 URL 是否安全,例如只允许 http/https,或者禁止 127.0.0.1、localhost。这种方式很容易漏掉各种等价表示。
// 不建议:只做字符串层面的简单判断
if (!url.startsWith("http")) {
throw new IllegalArgumentException("invalid url");
}
if (url.contains("127.0.0.1") || url.contains("localhost")) {
throw new IllegalArgumentException("blocked");
}更可靠的做法是使用标准 URL 解析库,提取 scheme、host、port,再进行规范化处理。注意:解析和实际连接之间仍可能存在 DNS 变化,因此还需要在连接前后结合 IP 校验。
2. 内网 IP 判断要覆盖完整范围
排查时常见的问题是只拦截 127.0.0.1,却忽略了其他私有地址和特殊地址段。至少应关注以下范围:
| 地址类型 | 示例 | 风险说明 |
|---|---|---|
| Loopback | 127.0.0.0/8、::1 | 访问本机服务 |
| Private IPv4 | 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 | 访问内网服务 |
| Link-local | 169.254.0.0/16、fe80::/10 | 云环境、链路本地资源 |
| Multicast / Reserved | 224.0.0.0/4、240.0.0.0/4 | 非公网目标,通常应拒绝 |
| Unspecified | 0.0.0.0、:: | 语义特殊,通常应拒绝 |
如果业务不需要访问任意外部地址,最推荐的方案不是维护黑名单,而是改为白名单:只允许访问明确的域名或对象存储来源。
3. 跳转链必须逐跳校验
很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只校验初始 URL,攻击者可以让初始 URL 指向一个公网域名,再通过跳转落到内网地址。
安全做法是关闭自动跳转,手动处理 Location,并在每一次跳转前重新解析、DNS 解析和 IP 范围校验,同时限制最大跳转次数。
# 测试环境中可以用 curl 观察跳转链,不建议对未授权目标扫描
curl -I -L --max-redirs 5 DNS Rebinding 不能忽略部分修复只在请求前解析一次域名,并判断解析结果是否为公网 IP。问题在于,请求真正发起时客户端可能再次解析域名,如果 DNS 返回结果发生变化,就可能绕过前置校验。
更稳妥的方式包括:
- 解析域名后,将连接目标固定为已校验的 IP;
- 校验 Host 头与 TLS SNI 的处理逻辑,避免破坏正常 HTTPS 校验;
- 连接建立后再次确认远端地址;
- 在网关或出网代理层做 egress 控制,拒绝访问内网网段。
单靠应用层校验很难覆盖所有边界,生产环境建议配合网络层策略。
关键步骤
步骤一:定位所有服务端出网点
代码审计时,可以先搜索常见 HTTP 客户端和网络请求函数。不同语言关键词不同,下面是一些常见线索:
# Java
RestTemplate
WebClient
HttpClient
OkHttpClient
URLConnection
# Node.js
axios
node-fetch
request
got
http.request
https.request
# Python
requests.get
requests.post
urllib.request
aiohttp
# PHP
curl_exec
file_get_contents
fopen
GuzzleHttp
不要只看“导入 URL”这类显性功能,头像下载、富文本图片转存、PDF 生成、Webhook 调试、RSS 抓取、Open Graph 预览都可能包含 SSRF 风险。
步骤二:确认输入到请求的完整链路
审计时建议画出数据流:
- 用户输入从哪个字段进入;
- 是否经过 URL decode、拼接、重定向、代理;
- 是否进入统一下载组件;
- 请求结果是否回显给用户;
- 异常信息是否暴露连接失败、超时、状态码等细节。
有些接口本身不回显响应内容,但会返回耗时、错误类型、HTTP 状态码,这也可能形成内网探测侧信道。修复时应统一错误响应,避免暴露过多细节。
步骤三:在授权环境中构造最小化验证
验证 SSRF 不需要访问任何真实内网资产。可以在本地或测试环境准备两个服务:一个模拟业务服务,一个模拟受保护的内网服务。
# 模拟内网服务,仅监听本机
python3 -m http.server 9000 --bind 127.0.0.1
# 在业务测试环境中提交 URL
302 的服务,Location 指向本机地址,再观察业务端是否跟随。# 仅用于本地授权测试的伪代码
HTTP/1.1 302 Found
Location: URL 安全校验组件不要在各个业务函数里重复写零散判断,建议封装成统一组件。核心逻辑包括:
- 仅允许 http 和 https;
- 禁止 URL 中携带用户名密码形式的 userinfo,降低解析歧义;
- 规范化 host,处理大小写、尾点、IDN;
- 解析域名得到 IP 列表,对每个 IP 做公网性判断;
- 限制端口,只允许 80、443 或业务明确需要的端口;
- 关闭自动重定向,逐跳校验;
- 限制超时、最大响应体大小和 Content-Type;
- 统一错误信息,不回显底层网络细节。
// 伪代码:表达校验思路,不绑定具体框架
function validateUrl(inputUrl) {
parsed = parseUrlStrictly(inputUrl)
if parsed.scheme not in ["http", "https"]:
reject("unsupported scheme")
if parsed.username or parsed.password:
reject("userinfo not allowed")
host = normalizeHost(parsed.host)
port = parsed.port or defaultPort(parsed.scheme)
if port not in [80, 443]:
reject("port not allowed")
ips = resolveAll(host)
if ips is empty:
reject("resolve failed")
for ip in ips:
if isPrivateOrSpecialAddress(ip):
reject("address not allowed")
return normalizedUrl
步骤五:增加网络层兜底
应用层校验是必要的,但不应是唯一防线。生产环境建议将需要访问外部资源的服务统一走出网代理,并在代理或防火墙层拒绝访问内网和云元数据地址。
# 示例:Linux 环境下思路示意,实际规则需结合网络拓扑评估
# 拒绝访问常见私有地址段
iptables -A OUTPUT -d 10.0.0.0/8 -j REJECT
iptables -A OUTPUT -d 172.16.0.0/12 -j REJECT
iptables -A OUTPUT -d 192.168.0.0/16 -j REJECT
iptables -A OUTPUT -d 127.0.0.0/8 -j REJECT
iptables -A OUTPUT -d 169.254.0.0/16 -j REJECT
如果运行在 Kubernetes 中,也可以通过 NetworkPolicy、服务网格或云厂商安全组限制 Pod 出站流量。关键是把“默认可访问所有地址”改成“只允许必要出口”。
修复后的回归测试清单
- 提交普通 HTTPS 图片地址,功能正常;
- 提交 http、https 以外协议,必须拒绝;
- 提交 127.0.0.1、0.0.0.0、localhost、内网 IP,必须拒绝;
- 提交解析到内网 IP 的域名,必须拒绝;
- 提交初始公网地址但 302 跳转到内网地址,必须拒绝;
- 跳转次数超过限制,必须拒绝;
- 超大响应体、慢响应、非预期 Content-Type,必须拒绝或中断;
- 错误响应中不出现内网 IP、端口状态、底层异常堆栈;
- 出网代理或网络策略能拦截应用层漏掉的内网访问。
注意点
- 不要把黑名单当作完整防护。黑名单适合作为补充,不适合作为主要策略。
- 不要只在前端校验。SSRF 是服务端发起请求,必须在后端和网络层控制。
- 不要信任 Content-Type。文件类型应结合响应头、文件头和解码结果判断。
- 不要无限制下载。设置连接超时、读取超时、最大字节数,避免资源消耗问题。
- 不要将下载组件暴露为通用代理。业务只需要图片,就不要支持任意 URL 转发。
- 云环境要特别关注元数据服务访问控制,建议启用更严格的元数据访问版本和实例级防护。
总结
SSRF 的修复重点不是补一个 if 判断,而是建立一套完整的服务端出网安全模型:输入规范化、协议和端口白名单、DNS 与 IP 校验、重定向逐跳检查、响应大小限制、统一错误处理,以及网络层兜底。
从长期维护角度看,建议把 URL 下载、Webhook 请求、远程资源导入等能力统一收敛到安全组件中,避免每个业务各写一套校验逻辑。这样后续遇到新的绕过形式或策略调整时,只需要修改一处,风险也更可控。
经验上,SSRF 最容易出现在“看起来只是下载一张图片”的功能里。审计时不要只盯高危接口,凡是服务端会根据用户输入访问外部地址的地方,都应该纳入检查范围。

代码审计、调用链与关键函数定位示意
推荐意见