背景
SSRF(Server-Side Request Forgery)这类问题在日常代码审计和应急排查中出现频率很高。它的危险点不在于“服务端发起了一次请求”本身,而在于请求目标、协议、重定向链路、DNS 解析结果、云环境元数据地址等因素叠加后,可能让一个普通的 URL 抓取功能变成内网探测入口。
这篇整理来自一次真实业务场景的复盘:某系统提供“远程图片抓取”和“URL 预览”能力,安全设备发现服务端对多个内网地址产生异常请求。最终确认问题并非单点漏洞,而是 URL 校验、重定向处理、DNS 解析和出网策略都存在缺口。
本文只讨论防护、审计和复现验证思路,不提供针对第三方系统的攻击操作指引。相关测试应在授权环境中进行。
问题场景
业务接口大致逻辑如下:用户提交一个 URL,后端请求该 URL,获取图片或网页标题,然后存储或展示给前端。常见代码形态包括:
POST /api/fetch-preview
Content-Type: application/json
{
"url": "
}看起来只是一个普通的服务端 HTTP 客户端调用,但风险点通常出现在以下位置:
- 只用字符串前缀判断 URL 是否以 http 或 https 开头。
- 只校验用户输入的原始域名,没有校验最终解析出的 IP。
- 允许 HTTP 客户端自动跟随 30x 重定向。
- 没有限制访问私网地址、回环地址、链路本地地址、云元数据地址。
- 没有设置合理的超时、响应大小限制和协议限制。
- 服务所在网络可以直接访问大量内网管理面。
技术分析
SSRF 防护的难点在于“看起来是外部 URL”的输入,最终可能在服务端侧变成访问内部资源的请求。这里需要重点关注几个细节。
1. URL 解析差异
不同语言、不同库对 URL 的解析行为并不完全一致。例如用户名字段、端口、编码字符、IPv6 表示法、十进制/八进制 IP 表示,都可能造成业务校验和实际请求目标不一致。
审计时不要只看这样的判断:
if (!url.startsWith("http")) {
throw new IllegalArgumentException("invalid url");
}这种方式无法确认协议、主机、端口和最终请求地址。更稳妥的方式是使用标准库解析 URL,并对解析结果做白名单或黑名单校验。
2. DNS 解析与重绑定风险
很多代码只判断 host 是否是某个外部域名,但实际连接前会再次进行 DNS 解析。如果解析结果变化,或者域名短时间内指向内网 IP,就可能绕过只基于域名的判断。
防护思路是:请求前解析域名,校验解析出的所有 IP;连接时尽量绑定已校验 IP;对于重定向后的新地址重复执行相同校验。
3. 自动重定向
HTTP 客户端默认跟随 301/302/307/308 重定向是常见隐患。即使初始 URL 是可信外部地址,重定向目标也可能落到内网地址或非预期协议。
建议关闭自动重定向,改为手动处理,每一跳都重新解析并校验:
// 示例:伪代码
response = httpClient.request(url, followRedirect = false)
if response.status in [301, 302, 307, 308]:
nextUrl = resolveLocation(url, response.headers["Location"])
validateUrl(nextUrl)
// 限制最大跳转次数,例如不超过 3 次4. 私网与特殊地址过滤
服务端请求应至少拒绝以下地址段:
| 类型 | 示例 |
|---|---|
| 回环地址 | 127.0.0.0/8、::1 |
| 私网地址 | 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 |
| 链路本地地址 | 169.254.0.0/16、fe80::/10 |
| 云元数据常见地址 | 169.254.169.254 |
| 本机或未指定地址 | 0.0.0.0、::、localhost |
注意不要只过滤字符串形式的 127.0.0.1。实际解析后的 IP 才是判断依据。
关键步骤:一次安全复现与定位流程
在授权测试环境中,可以按下面流程验证业务是否存在 SSRF 风险。
1. 确认入口和出网行为
先找到所有服务端请求外部 URL 的接口,例如图片上传、头像抓取、网页预览、Webhook 回调、PDF 生成、XML 解析、文件导入等。
可在测试环境使用一个可控的 HTTP 服务观察服务端是否会发起请求:
python3 -m http.server 8000如果业务服务器请求了该测试服务,可以从访问日志中确认来源 IP、User-Agent、请求路径和时间。
2. 验证协议和端口限制
重点确认是否只允许 http/https,是否限制端口范围。一般业务场景不应允许任意协议,也不应允许访问管理类端口。
# 只作为测试环境中的观测服务示例
nc -l 8081如果后端允许访问任意端口,至少说明缺少端口策略。生产环境建议仅允许 80、443 或业务明确需要的端口。
3. 验证重定向处理
可以在测试环境搭一个简单服务返回 302,观察后端是否继续跟随跳转。如果会跟随,需要确认每次跳转是否重新执行目标校验。
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", "
self.end_headers()
HTTPServer(("0.0.0.0", 9000), H).serve_forever()这里的 Location 仅使用公开安全域名示例。实际测试时不要指向未授权内网资产。
4. 检查响应处理限制
除了访问目标,响应处理也要关注。没有大小限制可能导致内存占用过高;没有类型检查可能把 HTML 当图片存储;没有超时可能导致线程阻塞。
建议设置:
- 连接超时:例如 2 到 5 秒。
- 读取超时:例如 5 到 10 秒。
- 最大响应体:例如图片抓取限制在 5MB 或业务可接受范围。
- Content-Type 白名单:例如 image/jpeg、image/png、image/webp。
- 禁止压缩炸弹:对解压后大小做限制。
代码侧防护示例
下面是一个偏通用的校验思路,重点是“解析 URL、限制协议、解析 IP、过滤特殊地址、控制重定向”。不同语言实现细节不同,但流程类似。
function validateTargetUrl(inputUrl):
url = parseUrl(inputUrl)
if url.scheme not in ["http", "https"]:
reject("unsupported scheme")
if url.username or url.password:
reject("userinfo not allowed")
if url.port not in [80, 443, null]:
reject("port not allowed")
ips = dnsResolveAll(url.host)
if ips is empty:
reject("dns resolve failed")
for ip in ips:
if isPrivate(ip) or isLoopback(ip) or isLinkLocal(ip) or isMetadataAddress(ip):
reject("forbidden address")
return url如果使用 Java,可以注意 HttpClient、OkHttp、Apache HttpClient 对重定向和 DNS 的处理方式;如果使用 Go,则要关注 net/http 的 CheckRedirect、DialContext;如果使用 Python requests,则需要禁用 allow_redirects 并手动处理。
# Python requests 示例:关闭自动重定向并限制超时
import requests
resp = requests.get(
url,
allow_redirects=False,
timeout=(3, 5),
headers={"User-Agent": "safe-fetcher/1.0"},
stream=True
)注意:上面只是请求侧片段,真正上线前还需要在请求前后都做目标校验和响应大小控制。
网络侧和平台侧加固
仅靠代码防护并不稳,尤其是历史系统较多时。更建议配合网络层策略。
- 服务出网走统一代理,由代理执行域名、IP、端口策略。
- 默认拒绝访问内网网段,仅对必要外部域名开放。
- 云环境中限制元数据服务访问,优先使用 IMDSv2 等更安全机制。
- 容器环境中避免业务 Pod 默认具备访问集群管理面的能力。
- 对服务端异常出网建立监控,例如访问 169.254.169.254、127.0.0.1、内网网段时告警。
日志与检测建议
复盘中比较有价值的日志字段包括:
- 用户提交的原始 URL。
- 解析后的 scheme、host、port。
- DNS 解析得到的 IP 列表。
- 最终连接 IP。
- 重定向链路。
- 响应状态码、Content-Type、响应大小。
- 请求耗时和异常信息。
这些字段不要直接暴露给普通用户,但应进入安全日志或审计日志。发生异常时,可以快速判断是误报、业务正常访问,还是存在被利用迹象。
常见误区
- 只检查 URL 字符串中是否包含 localhost,这种方式很容易漏掉变体。
- 只在入口校验一次,忽略重定向后的目标。
- 只校验域名,不校验 DNS 解析结果。
- 认为内网接口没有鉴权也没关系,实际上 SSRF 常常借服务端网络位置访问这些接口。
- 依赖 WAF 规则兜底,但服务端请求链路通常不完全经过入口 WAF。
总结
SSRF 的治理重点不是写一个简单的正则,而是把“服务端可访问的目标”收敛到业务真正需要的范围内。代码层要做严格 URL 解析、协议和端口限制、IP 地址校验、重定向控制、超时和响应大小限制;网络层要默认拒绝内网访问,出网统一治理;日志层要能还原最终请求目标和重定向链路。
如果系统里存在图片抓取、网页预览、Webhook、文件导入、PDF 生成这类功能,建议优先排查。很多 SSRF 风险不是一次明显的代码错误,而是多个“看起来合理的默认行为”叠加出来的。
推荐意见