服务端请求伪造(SSRF)是代码审计中比较常见的一类问题。它通常不是“某一行代码直接执行了命令”,而是应用把用户可控的 URL 交给了 HTTP 客户端,导致服务端替用户访问了原本不应暴露的网络资源。
这类漏洞的风险取决于应用所在网络环境、身份权限和出站访问策略。公网应用可能因此访问内网管理接口,云环境中还可能接触实例元数据服务。本文以安全测试和修复为目标,整理 SSRF 的审计思路、实验室验证方法以及工程上的防护要点。测试应仅限于授权环境,不要扫描或访问不属于自己的内网资源。
一、先理解 SSRF 的实际攻击面
典型业务场景包括:头像或图片远程下载、网页截图、PDF 转换、Webhook 回调、URL 预览、导入远程配置、Git 仓库同步,以及通过第三方服务抓取链接。这些功能本身并不一定存在漏洞,关键在于服务端是否对目标地址、协议、解析结果和跳转行为进行了严格限制。
一个常见的危险写法如下:
import requests
url = request.args.get("url")
response = requests.get(url, timeout=5)
return response.content问题不只在于没有白名单。即使业务方声称“只允许访问图片”,攻击者仍可能利用重定向、DNS 解析变化、非标准 IP 表示方式或 URL 解析差异绕过表面校验。
二、代码审计时的定位方法
审计时可以从“输入源—URL 处理—请求出口”三段链路入手,而不要只搜索某个函数名。
- 输入源:HTTP 参数、JSON 字段、HTTP Header、数据库中的可编辑字段、消息队列任务和后台导入文件。
- URL 处理:字符串拼接、URL 解析、域名校验、协议判断、重定向跟随、DNS 解析和 IP 判断。
- 请求出口:requests、urllib、OkHttp、Apache HttpClient、axios、curl、Go net/http 等客户端。
在 Java 项目中,可以重点关注 URL、URI、HttpClient、RestTemplate、WebClient、URLConnection 等调用点;在 Python 项目中,则重点关注 requests、urllib.request、httpx 和 aiohttp。审计时应继续向上追踪参数是否可控,向下确认是否自动跟随重定向、是否复用代理配置以及是否存在统一的出站网络封装。
还要注意间接 SSRF:代码表面上只接收一个域名或资源 ID,但后端可能根据该值从数据库取出完整 URL;或者应用调用第三方“截图、转码、抓取”服务,而该服务最终替应用发起请求。
三、实验室中的安全验证
验证目标应是证明“请求确实由服务端发出”,而不是访问真实内网系统。可以在本地启动一个专门的 HTTP 监听服务,只返回固定文本,并记录请求来源。
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
print("request from:", self.client_address[0], self.path)
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"ssrf-lab")
HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()然后在隔离的测试应用中提交本地测试地址,观察监听服务是否收到请求。更稳妥的验证方式是使用随机路径或随机请求头标记,例如 /probe-7f31,便于确认请求来自目标应用而不是浏览器或代理。
验证过程中不应直接访问云元数据地址、公司内网管理端口、数据库端口或其他未授权服务。对于漏洞报告,通常提供请求链路、服务端访问日志、影响范围和修复建议即可,无需扩大探测范围。
四、容易被忽略的绕过点
1. 重定向
应用可能先校验了一个允许访问的域名,但请求库随后自动跟随 30x 跳转到其他地址。因此,校验应作用于每一次跳转后的目标,或者在不需要时直接关闭自动跳转。
response = requests.get(
url,
timeout=(3, 5),
allow_redirects=False
)如果业务必须支持跳转,应限制跳转次数,并在每一跳重新执行协议、域名、解析地址和端口校验。
2. DNS 解析变化
只在请求前解析一次域名并判断其为公网地址,并不能完全解决问题。域名解析结果可能在校验与连接之间发生变化,也可能返回多个地址。生产实现应尽量使用统一的 DNS 解析和连接流程,在实际建立连接前再次确认目标地址属于允许范围。
3. IP 地址的多种表示方式
校验逻辑不能只判断字符串是否以 127. 或 10. 开头。IPv4 整数形式、十六进制形式、IPv4-mapped IPv6、压缩 IPv6 以及带前导零的表示,都可能造成不同组件之间的解析结果不一致。应使用标准库将地址规范化为 IP 对象,再按网段进行判断,避免自行编写字符串匹配。
4. URL 解析差异
不同语言和库对用户名信息、端口、空白字符、反斜杠及特殊编码的处理可能不同。不要用字符串截取方式提取主机名。应使用成熟 URL 解析器,并明确拒绝带用户名密码的 URL、非预期端口和非 HTTP(S) 协议。
五、一个相对稳妥的防护实现
下面示例展示了防护思路,而不是完整的生产组件。核心原则是:先限制协议,再解析主机,解析全部地址,拒绝私网和保留地址,最后通过受控客户端发起请求。
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {"https"}
ALLOWED_HOSTS = {"static.example.test", "img.example.test"}
def is_public_ip(value):
ip = ipaddress.ip_address(value)
return not (
ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_multicast or ip.is_reserved or ip.is_unspecified
)
def validate_url(raw):
parsed = urlparse(raw)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError("unsupported scheme")
if parsed.username or parsed.password:
raise ValueError("userinfo is not allowed")
if not parsed.hostname or parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("host is not allowed")
if parsed.port not in (443, None):
raise ValueError("port is not allowed")
addresses = socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM)
ips = {item[4][0] for item in addresses}
if not ips or not all(is_public_ip(item) for item in ips):
raise ValueError("resolved address is not allowed")
return parsed实际项目中,域名白名单通常比“允许所有公网地址”更可靠。若业务只能访问固定供应商,建议保存规范化后的域名和端口,并在服务端统一封装出站请求,避免每个业务模块各自实现一套不一致的校验。
六、网络层防护不能缺席
应用层校验存在复杂边界,网络层应作为第二道防线。可以从以下方面实施:
- 默认禁止应用服务器访问不必要的内网网段、管理网段和本地回环地址。
- 通过 egress 防火墙或代理限制出站协议、端口和目标网段。
- 将需要抓取外部资源的任务放入隔离的工作节点,使用最小权限的服务账号。
- 对出站 DNS、HTTP 代理和连接日志进行集中记录,保留目标域名、解析地址、端口、响应状态和任务标识。
- 云环境中按平台文档关闭不必要的元数据访问能力,并启用更严格的元数据访问保护和身份权限控制。
网络层规则不应只依赖域名,因为域名可能被重新解析,且代理、DNS 和应用自身的解析路径可能不同。更可靠的做法是同时约束域名、解析结果、出口代理和目标端口。
七、测试与审计中的注意点
- 不要把“返回了 200”直接等同于漏洞成立。需要确认请求由服务端发出,并判断响应内容是否会回显给用户。
- 区分盲 SSRF 和可回显 SSRF。盲 SSRF 也可能有风险,但影响证明方式通常依赖应用日志、受控回调或网络流量记录。
- 检查超时、响应大小和下载类型限制,避免 SSRF 之外进一步演变为资源耗尽。
- 文件下载功能还要考虑响应内容校验、文件名处理、存储路径和后续解析器风险。
- 修复后应回归测试直接访问、重定向、IPv6、异常端口、解析失败和多次解析等情况。
八、总结
SSRF 的本质不是简单的“用户能传 URL”,而是用户输入影响了服务端的网络边界。可靠的处理方式应同时覆盖输入校验、URL 规范化、DNS 与连接过程、重定向策略、出站网络控制和日志审计。
从审计角度看,最有效的路径是沿数据流追踪:找到可控输入,确认 URL 是否进入请求库,再核实解析、跳转和网络出口。修复时不要只增加一个字符串黑名单,而应优先采用固定目标白名单,并用网络隔离和最小权限降低单点失效带来的影响。
推荐意见