ssrf漏洞本质是服务器过度信任用户输入的url,未校验协议、主机、端口即发起请求。python中requests.get、urllib.request.urlopen、httpx、subprocess及第三方库远程加载等均易触发,须在每次调用前强制执行白名单校验,覆盖解析、dns归一化、私有ip拦截与精确域名匹配。

SSRF 漏洞在 Python Web 应用中不是“开了某个开关就能关掉”的问题,而是必须对每一次用户可控的 URL 输入做显式校验和约束。靠 urllib.parse 解析一下就放行,或只拦 file://、gopher://,基本等于没防。
Python 中哪些函数/库容易触发 SSRF?
只要最终调用了底层网络请求,且目标 URL 来自用户输入(哪怕经过简单拼接),就可能中招:
-
requests.get(url)、requests.post(url)—— 最常见,也是最危险的入口 -
urllib.request.urlopen(url)—— 原生库,不加校验时等同裸奔 -
httpx.get(url)—— 新兴异步库,同样不自带过滤 -
subprocess.run(['curl', url])或os.system(f'wget {url}')—— 更糟,协议不限、命令注入风险叠加
注意:requests 的 allow_redirects=True(默认)会放大风险——攻击者用 302 跳转绕过初始域名检查,所以校验必须在重定向链的每一步都生效,或直接禁用重定向。
白名单校验必须覆盖协议、主机、端口三要素
只校验域名(如 urlparse(url).netloc == 'api.example.com')是典型错误。攻击者可构造 http://api.example.com@192.168.1.100:8080 绕过;只拦 127.0.0.1 也无效,因为 localhost、[::1]、十进制 IP(2130706433)、URL 编码(%31%32%37%2e%30%2e%30%2e%31)都能绕过。
正确做法是:解析后取 urlparse(url).scheme、urlparse(url).hostname(不用 netloc,它含端口和用户信息)、urlparse(url).port or default_port,再逐项比对白名单:
from urllib.parse import urlparse
<p>def is_allowed_url(url: str) -> bool:
try:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
if not parsed.hostname:
return False</p><h1>显式解析 IP 并归一化</h1><pre class="brush:python;toolbar:false;"> import socket
try:
ip = socket.gethostbyname(parsed.hostname)
# 检查是否为私有地址(IPv4 + IPv6)
if ip.startswith(("10.", "172.16.", "192.168.", "127.")) or \
ip in ("::1", "0:0:0:0:0:0:0:1"):
return False
except socket.gaierror:
pass # DNS 失败不等于合法,但也不应因解析失败放行
# 白名单域名精确匹配(不含子域通配)
if parsed.hostname not in ("api.example.com", "cdn.example.net"):
return False
# 端口限制
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if port not in (80, 443, 8080):
return False
return True
except Exception:
return False
关键点:
- 用
socket.gethostbyname()而非正则匹配,防止 DNS rebinding 攻击 - 白名单必须是**精确域名列表**,不要写
*.example.com—— 子域控制权不在你手上 - 端口必须显式校验,不能依赖 scheme 默认值,因为
http://foo.com:22是合法 URL
为什么用 requests.Session() + mount() 不够安全?
有人试图用 requests.adapters.HTTPAdapter 拦截所有请求,或通过 Session.mount("http://", ...) 替换 adapter。这看似能统一管控,但实际无效:
- 拦截器在 DNS 解析之后执行,此时
127.0.0.1已被解析完成,无法阻止 - 若用户传入
http://attacker.com再 302 到内网地址,重定向由 requests 自动处理,mount 的 adapter 不会重新校验跳转目标 - 第三方库(如
pdfminer、pillow的远程加载)可能绕过 requests 生态,直接调用urllib
真正可靠的方案只有两个:
- 所有用户输入的 URL,在进入任何网络调用前,**强制走
is_allowed_url()校验**(推荐) - 用网络层隔离:在容器或宿主机上配置 eBPF 或 iptables 规则,禁止应用进程访问私有网段(运维侧兜底,开发侧不能依赖)
容易被忽略的 SSRF 入口点
除了显式的 URL 参数,这些地方常被遗漏:
-
Content-Type: multipart/form-data中的filename="http://..."—— 某些文件解析库会尝试打开该路径 - XML / RSS / Atom 解析时的
<link href="...">或<image></image>标签 - PDF / DOCX / Excel 文件元数据里的超链接字段,用
python-docx、pdfminer提取时触发 - Redis / MongoDB 连接字符串里嵌套的 URL(如
redis://user:pass@127.0.0.1:6379),若从配置拼接而来且用户可控部分参与构建
最麻烦的是:SSRF 防护没法“一次写完永久有效”。每次新增一个远程资源加载功能(比如加个“抓取网页标题”的按钮),就必须同步补上 URL 校验逻辑——漏一处,整条防线就形同虚设。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











