服务端请求伪造(SSRF)并不只存在于“图片抓取”这类明显场景中。Webhook 校验、远程文件导入、PDF 转换、头像代理、第三方接口探测等功能,只要服务端会根据用户输入发起网络请求,就可能成为审计重点。SSRF 的风险不在于某一个危险函数,而在于“用户可控地址”和“服务端网络权限”发生了组合。即使应用没有直接暴露敏感接口,只要部署环境能够访问内网管理面、云平台元数据服务或受信任的内部系统,问题的影响范围也可能被放大。一、审计场景与问题边界本文以一个常见的远程资源导入接口为例。接口接收一个 URL,服务端下载资源后进行解析或保存:POST /api/import
{
'source': ' />
}初步审计时,很多人只会检查是否使用了黑名单,例如拦截 localhost、127.0.0.1 或某些内网网段。这种方式通常不够可靠,原因包括:URL 解析存在差异,用户名、端口、编码和 IPv6 表示形式可能导致校验对象与实际连接目标不一致。域名解析结果可能变化,校验阶段与连接阶段不一定使用同一个地址。HTTP 客户端可能自动跟随重定向,初始地址安全不代表最终地址安全。应用所在网络本身可能存在代理、服务发现或容器网络,简单的公网与内网判断未必符合真实边界。因此,审计目标不应只是寻找“能否请求 127.0.0.1”,而应回答三个问题:用户能否影响请求目标;服务端请求使用了哪些协议和跳转行为;该请求在部署环境中具有什么网络权限。二、从代码中定位请求链路建议先从外部输入追踪到网络请求函数,而不是只搜索某一个函数名。常见的入口包括 request body、查询参数、表单字段、消息队列内容和数据库中可被用户间接控制的字段。在 Python 项目中,可以重点关注 requests、urllib、httpx、aiohttp 等调用;Java 项目则关注 URLConnection、HttpClient、WebClient、RestTemplate 以及各类文件下载工具。审计时需要继续追踪封装层,确认参数是否经过了二次解析或被统一客户端接管。def import_remote(url):
response = http_client.get(
url,
timeout=(3, 8),
allow_redirects=True
)
return parse_document(response.content)上面的代码有几个需要继续确认的点:url 是否完全来自用户输入,还是经过了允许列表校验。http_client 是否支持 file、ftp、gopher 等非预期协议。allow_redirects 是否可以关闭,或者每次跳转是否重新执行地址校验。解析响应前是否限制了响应大小、压缩展开比例和内容类型。请求是否经过统一出口代理,是否能够访问容器宿主机、云元数据或内部管理网段。三、地址校验的关键细节安全校验应当采用“先解析、后连接、连接时再次确认”的思路,而不是简单对原始字符串做关键词匹配。第一步是限制协议。对于远程资源导入这类业务,通常只允许 HTTPS;如果确实需要 HTTP,也应明确记录业务原因,并通过网络出口策略限制访问范围。协议、主机、端口和路径应当分别解析,不要依赖字符串前缀判断。第二步是规范化主机名并解析 DNS。解析结果中的每一个地址都应进行网段判断,至少覆盖回环地址、私有地址、链路本地地址、未指定地址、组播地址以及组织实际禁止访问的管理网段。IPv4-mapped IPv6 地址也不能被遗漏。from ipaddress import ip_address, ip_network
from urllib.parse import urlsplit
import socket
DENY_NETS = [
ip_network('127.0.0.0/8'),
ip_network('10.0.0.0/8'),
ip_network('172.16.0.0/12'),
ip_network('192.168.0.0/16'),
ip_network('169.254.0.0/16'),
ip_network('::1/128'),
ip_network('fc00::/7'),
ip_network('fe80::/10')
]
def is_denied(addr):
ip = ip_address(addr)
return any(ip in net for net in DENY_NETS)
def validate_target(raw_url):
parsed = urlsplit(raw_url)
if parsed.scheme not in ('http', 'https'):
raise ValueError('unsupported scheme')
if not parsed.hostname:
raise ValueError('missing hostname')
if parsed.username or parsed.password:
raise ValueError('userinfo is not allowed')
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
if port not in (80, 443):
raise ValueError('unsupported port')
addresses = {
item[4][0]
for item in socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM)
}
if not addresses or any(is_denied(addr) for addr in addresses):
raise ValueError('private or local address')
return parsed, addresses这段代码只用于说明审计思路,不能直接视为完整的生产级组件。实际实现还需要考虑 DNS 缓存、代理配置、IPv4 与 IPv6 的连接路径、异常处理以及出口网络策略。四、DNS 解析与连接之间的竞态仅在校验阶段解析一次 DNS 仍然存在竞态:校验时得到的是公网地址,真正建立连接时可能已经解析到其他地址。更稳妥的实现方式包括:由统一的安全 HTTP 客户端负责解析和连接,并在连接前后绑定校验结果。禁止应用自行使用环境代理,或明确配置代理并在代理侧执行目标地址策略。对域名解析结果设置短期缓存,同时在每次实际连接前再次检查地址。在网络层限制服务的出站访问,应用层校验失效时仍保留第二道边界。如果使用自定义 DNS 解析器,需要确认 HTTP 客户端没有在底层重新解析主机名。否则,应用层看到的地址与套接字真正连接的地址可能并不相同。五、重定向、端口与协议处理重定向是审计中最容易遗漏的路径。一个公网 URL 可以返回 3xx 响应,客户端如果自动跟随,最终请求目标可能已经发生变化。推荐的策略是默认关闭自动重定向;如果业务必须支持,应限制跳转次数,并对每个 Location 重新执行完整校验。def fetch(url):
current = url
for _ in range(3):
validate_target(current)
response = client.get(
current,
allow_redirects=False,
timeout=(3, 8)
)
if response.status_code not in (301, 302, 303, 307, 308):
return response
location = response.headers.get('Location')
if not location:
raise ValueError('redirect without location')
current = resolve_url(current, location)
raise ValueError('too many redirects')除重定向外,还应明确限制以下行为:禁止非业务需要的协议和特殊 URL scheme。限制端口范围,避免服务被用作任意 TCP 探测工具。拒绝带用户名和密码的 URL,避免日志泄露和解析歧义。限制 URL 长度、请求头大小、响应体大小和读取时间。禁止自动解压超大压缩包,避免将 SSRF 与资源耗尽问题叠加。六、测试方法:使用本地可控环境验证安全验证不需要访问真实的内部系统。可以在本地启动两个 HTTP 服务:一个模拟正常资源站点,另一个模拟重定向或异常响应,然后观察应用是否执行了预期的校验。python3 -m http.server 8081 --bind 127.0.0.1测试用例应覆盖正常公网地址、显式回环地址、私有地址、IPv6 地址、域名解析到本地地址、重定向到被禁止地址、非 HTTP 协议、非标准端口以及超大响应。测试重点是确认请求是否在网络连接前被拒绝,而不是依赖目标服务返回某种内容。在 CI 中,可以把这些场景做成单元测试和集成测试。对于解析逻辑,使用固定的 DNS 模拟器或可替换的解析函数,避免测试结果受到真实网络环境影响。七、修复方案应分层落地单一的输入过滤很难覆盖所有边界,建议将修复拆成应用层、客户端层和网络层三部分。应用层:只允许业务所需的协议、端口和域名;对每次重定向重新校验;限制请求和响应资源;记录请求摘要而不是完整敏感内容。客户端层:关闭不必要的代理继承和自动重定向,统一连接超时,禁止非预期协议,确保校验使用的解析结果与实际连接一致。网络层:通过防火墙、安全组或 egress gateway 仅允许访问必要的外部服务;禁止访问云元数据地址和内部管理网段;在容器环境中限制跨命名空间和宿主机访问。运维层:监控异常目标、短时间大量不同域名、异常端口和大量 3xx 跳转,并将这些事件与应用身份关联。安全边界的原则是:应用层负责判断“业务上允许访问什么”,网络层负责保证“即使应用判断错误,也不能访问什么”。八、审计中的常见误区只拦截 localhost 和 127.0.0.1,忽略其他内网地址、IPv6 和链路本地地址。只验证用户提交的初始 URL,不验证重定向后的地址。认为 HTTPS 等于安全,忽略 HTTPS 仍然可以访问不应访问的网络目标。只看响应内容,不关注请求是否能够到达内部服务。修复后没有检查代理、DNS、容器网络和云环境的实际出站路径。为了方便业务直接允许任意域名,再试图通过黑名单补救。总结SSRF 审计的核心不是寻找一组固定字符串,而是还原完整的请求链路:输入如何进入 URL 解析器,解析结果如何进入 DNS,连接是否经过代理,重定向是否重新校验,最终请求在部署环境中具有什么权限。在修复时,优先采用明确的允许列表、严格的协议和端口限制、每跳重定向校验以及网络出口控制。完成修复后,再通过本地可控环境和自动化测试覆盖解析、跳转、IPv4/IPv6、超时和资源限制等边界。这样得到的方案通常比单纯增加几个黑名单规则更稳定,也更容易在后续代码审计和版本迭代中保持有效。
推荐意见