3. SSRF漏洞成因深度剖析
SSRF(Server-Side Request Forgery,服务端请求伪造)的本质,是服务端把“本应由自己决定的目标地址”交给了外部输入来决定。攻击者并不需要直接访问内网,只要让存在缺陷的服务端代为发起请求,就能借助服务器的网络位置、身份凭证和信任关系,触达原本不可达的资源。理解成因,比记住漏洞名称更重要,因为绝大多数修复方案都直接对应某一种成因。
成因一:用户输入直接拼接URL
最典型的代码模式是把请求参数直接拼进 URL 字符串,然后交给 HTTP 客户端发起请求。例如一个“图片抓取”接口:
# 危险示例:目标地址完全由用户控制
@app.route("/fetch")
def fetch():
url = request.args.get("url")
resp = requests.get(url, timeout=3)
return resp.content
这里 url 没有任何约束,攻击者可传入 http://127.0.0.1:6379/、http://169.254.169.254/latest/meta-data/ 或 http://10.0.0.5:8080/admin。请求由服务器发出,因此回环地址、内网网段、云元数据地址都成为可达目标。更隐蔽的变体是“只拼接路径”:
# 危险示例:看似只拼路径,但 base 可被覆盖
base = "https://api.example.com/"
target = base + user_path # user_path = "@evil.com/" 或 "//evil.com/"
当 user_path 以 // 或 @ 开头时,URL 解析器可能把主机名替换为攻击者控制的域名,形成“拼接绕过”。因此,凡是目标地址来自请求参数、请求头(如 X-Forwarded-Host、Referer)、回调地址、Webhook 配置、文件导入链接的地方,都属于高风险输入点。
成因二:白名单校验不严与绕过
很多系统会加一层“域名白名单”,但校验方式决定了它是否有效。常见缺陷包括:只做字符串包含判断、只校验一次、校验与请求使用的解析结果不一致。
# 危险示例:字符串包含判断
if "example.com" in url:
requests.get(url)
# 攻击者可用 http://example.com.evil.com/ 或 http://evil.com/?x=example.com
更严重的是“校验后重定向”和“DNS重绑定”。前者先校验初始 URL 合法,再跟随 302 跳转到内网;后者让域名第一次解析为公网 IP 通过校验,第二次解析为 127.0.0.1 再发起请求。防御上应使用解析后的 IP 做判断,而不是原始字符串:
# 较安全示例:解析后校验 IP,并禁止重定向
import ipaddress, socket
from urllib.parse import urlparse
def is_public(host):
ip = socket.gethostbyname(host)
addr = ipaddress.ip_address(ip)
return not (addr.is_private or addr.is_loopback
or addr.is_link_local or addr.is_reserved)
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("scheme not allowed")
if not is_public(parsed.hostname):
raise ValueError("host not allowed")
requests.get(url, allow_redirects=False, timeout=3)
注意 allow_redirects=False 与 IP 校验必须同时存在,否则重定向仍可把请求带到内网。
成因三:重定向跟随
HTTP 客户端默认跟随 3xx 跳转,这是 SSRF 中最容易被忽视的放大器。攻击者只需提供一个“合法”的初始 URL,由自己控制的服务器返回 302 Location: http://169.254.169.254/latest/meta-data/,服务端就会自动跳转到内网。此时即使初始 URL 通过了白名单,最终请求目标也已改变。
因此,任何面向外部 URL 的请求都应显式关闭自动重定向,或在每一跳后重新执行完整的协议、主机、IP 校验。若业务必须跟随跳转,应限制跳转次数(如最多 3 次),并在每次跳转后重新校验目标地址。
成因四:协议滥用
SSRF 不只发生在 HTTP 上。当服务端使用支持多协议的客户端(如 curl、Python requests 配合自定义适配器、Java URLConnection)时,攻击者可切换协议读取本地文件或探测服务:
file:///etc/passwd:读取服务器本地文件;gopher://127.0.0.1:6379/_...:向 Redis 等 TCP 服务发送任意字节,可能写入计划任务或 SSH 公钥;dict://127.0.0.1:11211/stat:探测 Memcached;ftp://、ldap://:探测或利用内网服务。
协议滥用的根因是“只限制了主机,没有限制协议”。修复时应使用协议白名单,只允许 http 与 https,并在解析 URL 后再次确认 scheme,避免大小写、编码或空字节绕过。
代码模式与修复对照
把上述成因落到代码上,可以归纳为一组“危险模式—修复模式”的对照关系。下表给出常见写法与对应加固方式,便于在代码审计或开发自检时逐条核对。
| 危险模式 | 风险点 | 修复模式 |
|---|---|---|
requests.get(user_url) |
目标完全可控 | 解析 URL,校验 scheme 与解析后 IP |
if "trusted.com" in url |
字符串包含可被构造绕过 | 用 urlparse 取 hostname 做精确匹配 |
| 默认跟随 3xx | 重定向到内网 | allow_redirects=False,或每跳重新校验 |
| 仅限制主机,不限协议 | file/gopher/dict 滥用 | 协议白名单,仅允许 http/https |
| 无超时、无响应大小限制 | 被用于端口扫描或资源耗尽 | 设置 timeout 与最大读取字节数 |
易错点:只做“域名后缀匹配”并不安全。例如允许 *.example.com 时,攻击者可用 example.com.attacker.net 或 attacker.net#example.com 干扰解析。正确做法是先解析出 hostname,再判断其是否等于白名单域名或为其子域,并且对解析后的 IP 再做一次内网地址排除。
从成因到治理的落点
四类成因并非彼此独立:直接拼接是入口,白名单绕过是校验失效,重定向跟随是路径漂移,协议滥用是能力越界。实际漏洞往往是它们的组合,例如“白名单校验通过 + 302 跳转 + gopher 协议”即可完成对 Redis 的写入。因此修复不能只补一处,而应在请求发起前形成一条完整的校验链:先限制协议,再解析主机与 IP,再禁止或逐跳校验重定向,最后加上超时与响应大小限制。只有把“目标地址由谁决定”这个问题重新收回到服务端,SSRF 的成因才会被真正切断。