背景SSRF(Server-Side Request Forgery)是代码审计和漏洞复现里很常见的一类问题。它的风险不只在于“服务器帮用户发起请求”,更在于请求发起点往往位于内网、云环境或高权限网络区域,可能触达普通用户无法直接访问的资源。实际项目里,SSRF 经常出现在这些功能中:远程图片抓取、URL 预览、Webhook 回调、文件导入、PDF/截图生成、第三方资源转存、头像上传、站点连通性检测等。很多团队在修复时只做了简单黑名单,例如禁止 127.0.0.1,但真实场景里远远不够。本文只讨论在授权测试、代码审计和本地靶场中的复现与防护思路,不提供针对真实目标的攻击操作。典型场景假设一个业务提供“通过 URL 抓取图片并保存”的接口,后端接收用户传入的 URL,然后服务端下载资源。简化后的逻辑如下:POST /api/image/fetch
Content-Type: application/json
{
\"url\": \" />
}常见的后端实现可能是这样的:// 伪代码
String url = request.getParameter(\"url\");
byte[] data = httpClient.get(url);
storage.save(data);问题在于,如果没有严格限制 URL 的协议、解析结果、解析后的 IP 范围、跳转链路和响应大小,服务端就可能被诱导访问不该访问的地址。风险点拆解SSRF 的核心不在某一个 payload,而在“服务端请求边界失控”。审计时可以从以下几个维度判断风险。1. URL 协议是否受限只要允许用户控制完整 URL,就需要明确允许哪些协议。通常业务只需要 http 和 https。某些语言或库还可能支持 file、gopher、ftp、dict 等协议,默认行为不一定符合安全预期。推荐做法是使用白名单协议,而不是黑名单过滤:allowed_schemes = {\"http\", \"https\"}
parsed = parse_url(user_input)
if parsed.scheme not in allowed_schemes:
reject()2. 域名解析后的 IP 是否可信很多修复只检查 URL 字符串是否包含 127.0.0.1、localhost,这种方式容易失效。更可靠的方式是:先规范化 URL,再解析域名,最后检查解析到的所有 IP 是否落入内网、回环、链路本地、保留地址等范围。需要重点拦截的地址范围包括:127.0.0.0/8:本机回环地址10.0.0.0/8、172.16.0.0/12、192.168.0.0/16:常见私网地址169.254.0.0/16:链路本地地址,云环境元数据服务经常相关::1/128:IPv6 回环地址fc00::/7、fe80::/10:IPv6 私有或链路本地地址0.0.0.0/8、224.0.0.0/4、240.0.0.0/4:特殊或保留地址一个简化的 Python 校验示例:import ipaddress
import socket
from urllib.parse import urlparse
BLOCKED_NETS = [
ipaddress.ip_network(\"127.0.0.0/8\"),
ipaddress.ip_network(\"10.0.0.0/8\"),
ipaddress.ip_network(\"172.16.0.0/12\"),
ipaddress.ip_network(\"192.168.0.0/16\"),
ipaddress.ip_network(\"169.254.0.0/16\"),
ipaddress.ip_network(\"0.0.0.0/8\"),
ipaddress.ip_network(\"224.0.0.0/4\"),
ipaddress.ip_network(\"240.0.0.0/4\"),
ipaddress.ip_network(\"::1/128\"),
ipaddress.ip_network(\"fc00::/7\"),
ipaddress.ip_network(\"fe80::/10\"),
]
def is_blocked_ip(ip):
addr = ipaddress.ip_address(ip)
return any(addr in net for net in BLOCKED_NETS)
def resolve_and_check(url):
parsed = urlparse(url)
if parsed.scheme not in (\"http\", \"https\"):
raise ValueError(\"unsupported scheme\")
host = parsed.hostname
if not host:
raise ValueError(\"missing host\")
results = socket.getaddrinfo(host, None)
ips = {item[4][0] for item in results}
for ip in ips:
if is_blocked_ip(ip):
raise ValueError(\"blocked target ip\")
return True注意:生产环境里不能只在请求前检查一次。还要考虑 DNS Rebinding,即第一次解析为公网地址,通过校验后,实际连接时变成内网地址。更稳妥的方案是解析后绑定 IP 发起请求,并校验连接目标,或使用成熟的网络出口代理做统一拦截。3. HTTP 跳转链路是否重新校验很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只校验初始 URL,而跳转目标没有重新校验,依然可能导致访问越界。建议关闭自动跳转,手动处理每一次 Location,并对新的 URL 重新执行协议、域名、IP 范围校验,同时限制跳转次数。# 伪代码
max_redirects = 3
current_url = user_url
for i in range(max_redirects):
validate_url(current_url)
resp = http_get(current_url, allow_redirects=False)
if resp.status_code in [301, 302, 303, 307, 308]:
current_url = join_url(current_url, resp.headers[\"Location\"])
continue
return resp
reject(\"too many redirects\")4. 响应大小和超时是否有限制SSRF 不一定只带来内网访问风险,也可能造成资源消耗。比如下载超大文件、慢响应、无限流式响应,都可能拖垮业务线程池或占满磁盘。建议至少设置以下限制:连接超时和读取超时,例如 2 到 5 秒最大响应体大小,例如图片抓取限制为 5MB 或 10MB最大重定向次数,例如不超过 3 次限制 Content-Type,但不要只依赖响应头流式读取时边读边计数,超过阈值立即中断# 伪代码
max_size = 5 * 1024 * 1024
total = 0
for chunk in response.iter_content(8192):
total += len(chunk)
if total > max_size:
raise ValueError(\"response too large\")
write(chunk)5. 云环境元数据服务需要重点关注在云主机或容器环境中,元数据服务通常位于链路本地地址段。不同云厂商实现不同,但共同点是:应用如果能通过 SSRF 访问元数据接口,可能读取到实例相关信息或临时凭证。因此,在云环境里不能只依赖应用层校验,建议同时做网络层限制:应用所在安全组或防火墙限制访问元数据地址容器环境中限制 Pod 到元数据服务的访问开启云厂商提供的更安全元数据访问模式,例如要求 token 或限制 hop limit业务服务尽量使用最小权限的实例角色代码审计时的排查方法审计 SSRF 时,我通常先找“可控 URL 进入网络请求”的链路。关键不是搜索某一个函数,而是跟踪参数来源和最终 sink。可以关注这些关键词和调用点:语言/场景常见调用点关注点JavaURL.openConnection、HttpClient、OkHttp、RestTemplate是否校验协议、跳转和目标 IPPHPcurl_exec、file_get_contents、fopen是否允许远程 URL、是否限制协议Pythonrequests、urllib、httpx是否默认跟随跳转、是否设置超时Node.jsaxios、node-fetch、request是否限制重定向和私网地址Gohttp.Get、http.Client是否自定义 Transport 做地址校验常见业务参数名也值得留意:url、link、target、callback、redirectimage、avatar、file、import_urlwebhook、notify_url、callback_urlpreview、fetch、download、proxy如果是白盒审计,可以从路由入口开始做数据流分析:用户输入是否经过统一校验、是否被拼接成 URL、是否进入 HTTP 客户端、是否可能通过重定向改变请求目标。安全复现建议在授权测试中,建议搭建本地受控环境验证问题,不要直接对真实内网资源做探测。一个简单方法是准备两个本地服务:一个模拟业务服务,一个模拟内部资源。# 模拟内部资源服务,仅监听本机
python3 -m http.server 9000 --bind 127.0.0.1业务服务如果存在 SSRF 风险,传入一个指向本地资源的 URL,就能在本地日志中看到请求是否由服务端发起。这个过程能验证漏洞存在性,又不会影响第三方系统。也可以使用 DNS 日志平台或自建日志服务器确认“服务端是否发起了请求”,但不要继续扩大到端口扫描、凭证读取等高风险动作。漏洞报告里通常只需要证明服务端可控请求成立,并说明潜在影响即可。修复方案建议实际修复时,不建议只在业务代码里零散添加字符串过滤。更推荐做成统一组件或统一出口代理,所有远程资源抓取都走同一套策略。一个相对稳妥的修复清单:只允许 http、https 协议对 URL 做标准解析,不使用手写正则解析完整 URL解析域名后校验所有 A、AAAA 记录禁止访问私网、回环、链路本地、保留地址每次重定向后重新校验目标限制超时、响应大小、重定向次数限制可访问端口,通常只允许 80 和 443,必要时按业务白名单扩展关键业务使用域名白名单,而不是允许任意外部 URL云环境增加网络层和 IAM 最小权限控制记录安全日志,包括原始 URL、解析 IP、最终 URL、请求结果容易踩坑的点只过滤 localhost,但没有处理 127.0.0.1、IPv6、域名解析结果只校验第一次请求,没有校验重定向后的 Location认为 Content-Type 是 image/png 就一定是图片,实际响应头可伪造没有设置超时,导致线程被慢响应拖住没有限制下载大小,导致磁盘或内存被消耗在微服务环境中忽略服务发现、内部管理端口和本机 agent修复逻辑分散在多个接口,后续新功能又绕开校验总结SSRF 的防护重点不是记住某些特殊地址写法,而是建立一条清晰的服务端出网边界:哪些协议能用、哪些地址能访问、跳转是否可信、响应能消耗多少资源、云环境是否有额外隔离。从代码审计角度看,发现 SSRF 的关键是找到“用户可控输入到服务端网络请求”的完整链路;从修复角度看,最好把校验逻辑沉到统一组件或出口层,避免每个业务各写一套不完整的过滤。这样后续新增 URL 抓取、Webhook、资源转存等功能时,也能复用同一套安全边界。
推荐意见