跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器访问一个 URL”这么简单。真正难处理的地方在于:业务通常允许服务器主动访问外部资源,而攻击者又可能影响目标地址、请求协议、重定向链或解析结果。两者边界一旦没有定义清楚,就容易形成从内网探测、云元数据访问到敏感信息泄露的风险。本文以一次常见的“远程图片抓取”功能为例,整理如何从代码、日志和网络行为定位 SSRF 根因,并给出适合工程落地的修复方案。示例均以本地实验环境和防御验证为前提,不涉及对第三方系统的探测。一、问题场景:业务需求本身并不等于漏洞假设系统提供以下接口:用户提交图片地址,服务端下载图片后保存到对象存储或本地缓存。POST /api/image/import
{
\"url\": \" /> }后端最初的实现可能类似这样:import requests

def import_image(url):
response = requests.get(url, timeout=10)
response.raise_for_status()
return save_to_storage(response.content)这段代码的功能是成立的,但安全边界几乎没有定义。只要调用方可以控制 url,服务端就可能访问:本机回环地址、内网地址或容器网段;仅对服务器开放的管理接口;云环境中的实例元数据服务;经过重定向后才出现的内部地址;体积很大的响应,导致内存、磁盘或连接资源被消耗。因此,SSRF 的判断重点不是“是否调用了 HTTP 客户端”,而是“外部输入是否能影响服务端的出站请求,以及请求目标和响应是否受到有效约束”。二、先看证据:代码审计与日志分析如何互相印证实际排查时,不建议只依赖某一条异常日志。比较稳妥的方式是把入口、解析、请求、重定向和响应处理串起来看。1. 从输入源追踪到请求函数代码审计可以重点搜索以下模式:HTTP 客户端调用,例如 requests、axios、Java HttpClient、Go net/http;参数名为 url、uri、callback、webhook、image、feed、proxy 的接口;将用户输入拼接到命令行、模板或配置中的逻辑;自动跟随重定向、自动解析 DNS、上传或下载文件的封装函数。审计时需要继续向下追踪,而不是看到一个 URL 校验函数就直接判定安全。常见问题包括:校验使用的 URL 与实际请求使用的 URL 不同;校验后又发生了 DNS 解析;底层客户端默认跟随重定向;代理配置改变了实际出站路径。2. 结合出站日志确认实际行为如果系统有统一的出站代理或网络流量审计,建议至少记录以下字段:字段用途业务请求 ID关联用户请求、应用日志和出站请求原始 URL 的规范化结果确认解析前后的主机、端口和协议最终连接 IP识别域名解析到公网还是私网地址是否发生重定向判断首个 URL 安全但后续跳转危险的情况响应状态、长度和耗时辅助识别探测、超大响应和连接耗尽日志中如果只记录用户提交的域名,而不记录最终连接地址,往往不足以还原问题。特别是在 DNS 变化、代理转发或多次重定向的情况下,原始 URL 与实际访问目标可能完全不同。三、核心技术点:为什么简单的字符串过滤不可靠1. URL 解析存在语义差异不同语言和库对 URL 的解析规则并不完全一致。一个组件认为主机名是某个值,另一个组件可能把同一字符串解析成不同的用户信息、端口或路径。若安全校验和实际请求使用不同的解析器,就容易出现判断不一致。建议在进入业务逻辑后只保留一次规范化解析结果,并使用同一解析结果完成协议、主机、端口和路径检查。不要通过字符串包含、前缀匹配或简单正则表达式判断目标是否安全。2. DNS 解析与连接之间存在时间差典型的风险流程是:程序先解析域名并确认结果属于公网地址,稍后 HTTP 客户端再次解析该域名并建立连接。两次解析之间如果结果发生变化,前面的判断就失去了意义。更稳妥的做法是:在受控网络出口统一执行域名解析和访问策略;应用侧对解析结果做校验,但不要把它视为唯一防线;尽量避免由应用直接决定任意出站目标;对高风险场景使用固定出口代理,并在代理层再次校验目标地址。3. 重定向会绕过首地址校验如果应用只检查第一次请求的地址,却允许客户端自动跟随 301、302 或 307,服务端实际访问的可能是另一个地址。安全策略应明确是否允许重定向;如果业务确实需要,应限制跳转次数,并对每一次跳转后的目标重新进行完整校验。4. 协议不应默认放开远程资源抓取通常只需要 HTTPS,部分兼容场景才需要 HTTP。不要把支持 file、ftp、gopher 等协议交给通用请求库自动处理,也不要把用户输入直接传入支持多协议的下载器。协议白名单应在业务层和网络出口层同时执行。四、一个相对稳妥的校验流程以下示例展示的是防御性实现思路:只允许 HTTPS,限制端口,解析主机后拒绝本地及私有地址,并关闭自动重定向。生产环境还应结合出口代理和网络策略。from ipaddress import ip_address
from urllib.parse import urlparse
import socket
import requests

ALLOWED_PORTS = {443}
BLOCKED_NETWORKS = (
'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',
'fc00::/7',
'fe80::/10'
)

def is_blocked_ip(value):
ip = ip_address(value)
return ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_reserved

def validate_target(raw_url):
parsed = urlparse(raw_url)

if parsed.scheme != 'https':
raise ValueError('unsupported scheme')

if parsed.username or parsed.password:
raise ValueError('userinfo is not allowed')

if not parsed.hostname:
raise ValueError('missing hostname')

port = parsed.port or 443
if port not in ALLOWED_PORTS:
raise ValueError('unsupported port')

addresses = socket.getaddrinfo(
parsed.hostname,
port,
type=socket.SOCK_STREAM
)

for item in addresses:
resolved_ip = item[4][0]
if is_blocked_ip(resolved_ip):
raise ValueError('non-public address')

return parsed

def fetch_image(raw_url):
target = validate_target(raw_url)

response = requests.get(
target.geturl(),
timeout=(3, 8),
allow_redirects=False,
stream=True,
headers={'Accept': 'image/*'}
)

if response.status_code in (301, 302, 303, 307, 308):
raise ValueError('redirect is not allowed')

content_type = response.headers.get('Content-Type', '')
if not content_type.startswith('image/'):
raise ValueError('unexpected content type')

return read_limited_body(response, 5 * 1024 * 1024)这个示例仍然不是完整的通用库,原因在于网络安全策略无法只靠一段应用代码解决。比如应用与 DNS 服务之间的行为、HTTP 代理、IPv4/IPv6 双栈、连接复用以及底层连接时机,都可能影响最终结果。五、响应处理同样重要:SSRF 不只是“能不能连上”很多实现只关注目标地址,却忽略了响应本身带来的风险。1. 限制响应大小必须使用流式读取,并设置明确的上限。不能先把整个响应读入内存,再判断文件大小。对图片导入这类业务,可以同时限制:最大响应字节数;连接建立超时和读取超时;最大重定向次数;单个用户或 IP 的并发任务数;任务总执行时间和队列长度。2. 不要只信任 Content-Type响应头中的 Content-Type 可以由远端服务自行设置,不能作为唯一依据。下载后应结合文件头、解析库和实际格式进行检查,并拒绝包含异常结构的文件。图像处理库也应及时更新,因为文件解析本身可能引入独立的内存安全问题。3. 避免把响应原样返回给用户如果接口把远端响应直接透传给浏览器,可能带来响应头注入、缓存污染、跨域误配置或敏感内容暴露。更好的方式是:服务端完成格式校验后只保存必要内容,再由受控的静态资源域名提供访问。六、网络层加固:把应用当作不可信客户端应用层校验容易遗漏边界,网络层应该提供最后一道约束。推荐采用以下分层措施:默认拒绝应用容器或主机访问内网管理网段;限制只允许访问明确的 DNS、代理和业务目标;将远程抓取功能放入独立的低权限工作节点;工作节点不携带云平台实例角色或长期凭据;通过出口代理记录域名、最终 IP、端口和响应大小;对异常的目标数量、失败比例和响应耗时设置告警。在云环境中,不能把“应用没有读取元数据的代码”当作充分条件。只要工作负载具备访问相关地址的网络路径,就应在实例角色、元数据服务访问策略和网络访问控制层面同时收紧。七、如何在测试环境安全验证验证修复时,优先使用自建的测试服务,不要对外部地址或第三方内网进行探测。可以准备三个本地服务:正常返回图片的公网模拟服务;返回 302 的重定向服务;返回超大响应或慢速响应的测试服务。然后围绕以下断言编写自动化测试:def test_only_https_is_allowed():
assert validate_target(' /> expect_error('

def test_redirect_is_not_followed():
response = fetch_image(' /> assert response.status_code not in (200, 206)

def test_response_size_is_limited():
expect_error(' localhost:内网地址并不只有 localhost,还包括私网、链路本地、保留地址和内部 DNS 解析结果。只判断字符串是否包含内网 IP:这无法覆盖域名解析、IPv6、端口和重定向场景。只校验首次 URL:跟随重定向后,实际目标可能发生变化。只在前端限制输入:前端校验可以改善体验,但不能替代服务端策略。只把请求超时时间调小:超时能降低部分资源消耗,但不能解决越权访问。只依赖 WAF 规则:SSRF 的最终目标由解析和网络路径决定,单纯依赖字符串规则容易漏检。九、排查清单是否存在由用户控制的 URL、主机名、回调地址或导入地址?URL 校验与实际请求是否使用同一个规范化解析结果?是否限制了协议、端口和重定向?域名解析结果是否经过私网、回环、链路本地和保留地址检查?是否存在 DNS 解析与连接之间的竞态窗口?是否限制响应大小、连接超时、读取超时和并发数量?是否记录最终连接 IP、重定向链和响应元数据?应用所在网络是否具备访问不必要内网资源的能力?工作节点是否拥有云凭据、管理权限或过大的文件系统权限?是否有针对异常出站请求的监控和告警?总结SSRF 的根因通常不是某一个过滤条件缺失,而是业务对“服务器可以访问哪些目标”没有形成明确、可执行的安全策略。代码层需要完成协议、端口、主机和响应约束,网络层需要限制出站路径,日志层需要保留足够证据,测试层则要覆盖重定向、解析差异、超时和资源消耗等边界。对于远程资源抓取、Webhook、在线预览和第三方回调等功能,建议从设计阶段就采用白名单或固定目标集合,而不是先允许任意 URL,再不断补充黑名单。能把目标范围缩小到业务真正需要的程度,通常比编写一套复杂的字符串过滤规则更可靠。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…