CSDN 原文镜像
本文为作者 CSDN 博客的全文镜像,原文发布于 2026-06-10。为适配本站结构,仅补充了站内元数据与来源说明,正文主体保持原文内容。
- 原文链接:https://blog.csdn.net/m0_63309778/article/details/161862209
- 站内分区:工程实践 / SSRF 安全

一文讲清 SSRF:从漏洞原理到工程化防护方案
在 Web 安全中,SSRF 是一个非常典型、也非常容易被低估的漏洞。
很多开发者第一次接触 SSRF 时,会觉得它只是“后端请求了一个用户传入的 URL”。但在真实系统中,SSRF 的影响远不止如此。它可能被用来访问内网服务、探测端口、读取云厂商元数据、绕过防火墙,甚至进一步扩大为权限泄露和横向移动。
本文会从工程实践角度,系统介绍 SSRF 的基本原理、常见攻击面、防护难点,以及一套相对完整的服务端防护方案。
1. 什么是 SSRF?
SSRF,全称是 Server-Side Request Forgery,中文通常叫“服务端请求伪造”。
它的核心问题是:
攻击者无法直接访问某个目标资源,但可以诱导服务端替他访问。
举个简单例子,假设系统提供了一个“根据 URL 抓取网页内容”的功能:
用户输入 URL
↓
服务端请求该 URL
↓
服务端把内容返回给用户正常情况下,用户可能输入:
https://example.com/article/123但攻击者可能输入:
http://127.0.0.1:8080/admin
http://localhost:6379
http://169.254.169.254/latest/meta-data/如果后端没有任何限制,就会变成:
攻击者
↓
业务服务器
↓
内网服务 / 本机端口 / 云元数据服务这就是 SSRF 的本质:攻击者控制了服务端请求的目标地址。
2. SSRF 为什么危险?
SSRF 危险的原因在于,服务端通常比外部用户拥有更高的网络权限。
外部攻击者可能无法访问内网服务,但业务服务器可以。攻击者只要能让服务器发起请求,就可能借助服务器的身份进入本来无法触达的网络区域。
常见风险包括:
| 风险类型 | 说明 |
|---|---|
| 访问本机服务 | 如 127.0.0.1、localhost 上的管理接口 |
| 访问内网服务 | 如数据库、Redis、Nacos、Elasticsearch、Prometheus 等 |
| 探测内网端口 | 通过响应差异判断内网服务是否存在 |
| 访问云元数据服务 | 获取实例信息、临时凭证、角色权限等敏感数据 |
| 绕过网络边界 | 借助服务器突破防火墙、VPN、ACL 等限制 |
| 敏感信息泄露 | 返回配置、环境变量、Token、密钥等 |
| 扩大攻击面 | 作为进一步攻击内网系统的跳板 |
SSRF 的麻烦之处在于:它不是单纯的输入校验问题,而是涉及 URL 解析、DNS、HTTP 重定向、网络隔离、云环境权限等多个层面。
3. 哪些功能容易产生 SSRF?
只要系统存在“服务端根据用户可控输入去请求外部资源”的逻辑,就需要考虑 SSRF。
常见场景包括:
3.1 URL 内容抓取
例如:
输入文章链接,系统自动抓取正文
输入图片链接,系统自动下载图片
输入网页地址,系统自动生成预览
输入远程文件地址,系统自动导入文件这类功能是 SSRF 的高发区域。
3.2 Webhook / Callback
很多系统允许用户配置回调地址:
任务完成后,请求用户配置的 callbackUrl如果没有限制,攻击者可以把 callbackUrl 配成内网地址,让服务器主动访问内部系统。
3.3 图片代理和文件代理
有些系统为了避免前端跨域,会提供图片代理功能:
/image-proxy?url=https://example.com/a.png后端读取 url 参数,再把图片内容返回给前端。
这个功能如果不做校验,很容易被改成:
/image-proxy?url=http://127.0.0.1:8080/admin3.4 HTML 转 PDF / Markdown 渲染 / 富文本解析
这类场景经常被忽略。
例如系统支持 HTML 转 PDF,HTML 中可能包含:
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://127.0.0.1:8080/internal<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>渲染器在生成 PDF 时,会自动去加载图片、CSS、字体等远程资源。即使业务代码没有显式请求 URL,底层渲染工具也可能触发 SSRF。
3.5 文件上传后的自动解析
比如上传一个文档,系统自动解析其中的远程图片、外部链接、XML 实体、模板引用等,也可能触发服务端请求。
4. SSRF 防护为什么不能只靠黑名单?
很多人第一反应是:
禁止 127.0.0.1
禁止 localhost
禁止 169.254.169.254这当然有用,但远远不够。
因为 SSRF 可以通过很多方式绕过简单黑名单,例如:
使用域名解析到内网 IP
使用 IPv6 地址
使用特殊 IP 写法
使用短地址、整数地址、八进制地址
使用 DNS Rebinding
使用 302 重定向跳转到内网
使用 CNAME 指向内网地址
使用大小写、编码、URL 解析差异绕过例如你禁止了:
127.0.0.1但攻击者可能使用:
localhost
127.1
0.0.0.0
[::1]
某个解析到 127.0.0.1 的域名所以 SSRF 防护的核心不是“写几个黑名单字符串”,而是构建一整套请求安全边界。
5. SSRF 防护的总体思路
更合理的 SSRF 防护应该是多层防御:
用户输入
↓
URL 解析与规范化
↓
协议校验
↓
域名 / IP 校验
↓
端口校验
↓
DNS 解析结果校验
↓
重定向校验
↓
统一安全 HTTP 客户端
↓
网络层出站控制
↓
日志审计与告警核心原则有三点:
1. 不要信任用户传入的 URL
2. 不要让业务服务拥有任意访问网络的能力
3. 不要只依赖代码校验,必须配合网络层隔离6. 第一层防护:统一收口外部请求
最危险的写法是业务代码中到处直接请求用户传入的 URL。
错误示例:
<span class="token class-name">String</span> url <span class="token operator">=</span> request<span class="token punctuation">.</span><span class="token function">getParameter</span><span class="token punctuation">(</span><span class="token string">"url"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">String</span> result <span class="token operator">=</span> restTemplate<span class="token punctuation">.</span><span class="token function">getForObject</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token class-name">String</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>这样的问题是:
任何业务代码都可能绕过安全检查
安全规则分散在多个地方
后续难以审计
新增功能容易再次引入 SSRF更推荐的方式是封装统一组件:
SafeUrlValidator
SafeHttpClient
ExternalFetchService
WebhookDispatcher
ImageProxyService所有服务端请求外部 URL 的逻辑,都必须走统一的安全组件。
推荐结构:
业务代码
↓
SafeUrlValidator 校验 URL
↓
SafeHttpClient 发起请求
↓
Egress Proxy / 网络出口控制
↓
目标站点这样可以把 SSRF 防护逻辑集中到一个地方维护,而不是散落在各个业务模块中。
7. 第二层防护:优先使用白名单
SSRF 防护中,白名单通常比黑名单更可靠。
7.1 强白名单
如果业务场景比较明确,最推荐的方式是只允许访问指定域名。
例如:
<span class="token key atrule">ssrf</span><span class="token punctuation">:</span>
<span class="token key atrule">allowedDomains</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> example.com
<span class="token punctuation">-</span> api.example.com
<span class="token punctuation">-</span> static.example<span class="token punctuation">-</span>cdn.com校验逻辑:
用户输入 URL
↓
解析 host
↓
判断 host 是否在 allowedDomains 中
↓
不在白名单则拒绝强白名单适合这些场景:
固定第三方接口调用
固定资源下载源
固定合作方域名
内部配置好的可信站点这种方式安全性最高,维护成本也相对可控。
7.2 弱白名单 + 内网地址拦截
如果业务必须支持用户输入任意公网 URL,就不能只依赖域名白名单。这时至少要做:
只允许 http / https
禁止访问内网 IP
禁止访问本机地址
禁止访问云元数据地址
禁止访问特殊网段
限制端口
限制重定向
限制响应大小
限制超时时间但需要注意:支持“任意公网 URL”本身就意味着更大的风险,因此更应该配合网络层隔离。
8. 第三层防护:限制 URL 协议
服务端请求外部资源时,通常只应该允许:
http
https应该禁止:
file://
ftp://
gopher://
dict://
ldap://
jar://
netdoc://
data:
mailto:尤其是 file:// 这类协议,可能导致读取本地文件。
示例校验逻辑:
<span class="token class-name">URI</span> uri <span class="token operator">=</span> <span class="token constant">URI</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>rawUrl<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">String</span> scheme <span class="token operator">=</span> uri<span class="token punctuation">.</span><span class="token function">getScheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token string">"http"</span><span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span>scheme<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span><span class="token string">"https"</span><span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span>scheme<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Unsupported URL scheme"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>不要用简单的字符串判断,例如:
<span class="token keyword">if</span> <span class="token punctuation">(</span>url<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"http"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">// allow</span>
<span class="token punctuation">}</span>因为 URL 解析可能存在各种边界情况,应该使用标准 URL / URI 解析库进行处理。
9. 第四层防护:限制端口
如果没有特殊业务需求,建议只允许:
80
443危险端口包括:
22 SSH
2375 Docker API
3306 MySQL
5432 PostgreSQL
6379 Redis
9200 Elasticsearch
11211 Memcached
8848 Nacos
9000 对象存储服务
9001 管理控制台
10250 Kubelet推荐配置:
<span class="token key atrule">ssrf</span><span class="token punctuation">:</span>
<span class="token key atrule">allowedPorts</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token number">80</span>
<span class="token punctuation">-</span> <span class="token number">443</span>处理默认端口时也要注意:
http 默认端口是 80
https 默认端口是 443例如:
<span class="token keyword">int</span> port <span class="token operator">=</span> uri<span class="token punctuation">.</span><span class="token function">getPort</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>port <span class="token operator">==</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
port <span class="token operator">=</span> <span class="token string">"https"</span><span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span>uri<span class="token punctuation">.</span><span class="token function">getScheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token number">443</span> <span class="token operator">:</span> <span class="token number">80</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>allowedPorts<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>port<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Port not allowed"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>10. 第五层防护:禁止访问内网和特殊地址
服务端应该禁止访问以下类型的地址:
127.0.0.0/8 Loopback 地址
10.0.0.0/8 私有地址
172.16.0.0/12 私有地址
192.168.0.0/16 私有地址
169.254.0.0/16 Link-local 地址
0.0.0.0/8 当前网络地址
100.64.0.0/10 Carrier-grade NAT
224.0.0.0/4 组播地址
240.0.0.0/4 保留地址
::1/128 IPv6 Loopback
fc00::/7 IPv6 Unique Local Address
fe80::/10 IPv6 Link-local尤其要注意:
127.0.0.1
localhost
0.0.0.0
169.254.169.254
10.x.x.x
172.16.x.x - 172.31.x.x
192.168.x.x169.254.169.254 是云环境中特别敏感的地址,很多云厂商的实例元数据服务都和这个地址有关。一旦被 SSRF 访问,可能造成云资源凭证或实例信息泄露。
11. 第六层防护:DNS 解析结果校验
很多系统只检查 URL 字符串,这是不够的。
攻击者可以使用一个看起来正常的域名,但该域名解析到内网地址。
例如:
http://safe-looking-domain.com解析结果可能是:
127.0.0.1
10.0.0.10
192.168.1.100因此,正确流程应该是:
解析域名
↓
获取所有 A / AAAA 记录
↓
逐个判断是否属于内网或特殊地址
↓
只要有一个非法地址,就拒绝Java 示例:
<span class="token class-name">InetAddress</span><span class="token punctuation">[</span><span class="token punctuation">]</span> addresses <span class="token operator">=</span> <span class="token class-name">InetAddress</span><span class="token punctuation">.</span><span class="token function">getAllByName</span><span class="token punctuation">(</span>host<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token class-name">InetAddress</span> address <span class="token operator">:</span> addresses<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">isBlockedAddress</span><span class="token punctuation">(</span>address<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Blocked target address"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>注意:仅使用 InetAddress.isSiteLocalAddress() 还不够,因为它不能覆盖所有危险地址。真实生产环境中,建议使用 CIDR 匹配库对完整网段进行判断。
12. 第七层防护:防止 DNS Rebinding
DNS Rebinding 是 SSRF 防护中的一个难点。
它的大致思路是:
第一次解析域名时,返回公网 IP
安全校验通过
实际请求或后续请求时,域名变成内网 IP
绕过校验这类问题本质上属于 TOCTOU 问题:
Time Of Check
检查时是安全的
Time Of Use
使用时已经变了防护建议:
1. 请求前解析域名,并校验所有解析结果
2. 不要只在任务创建时校验一次,真正请求前也要校验
3. 每次重定向后都重新解析和校验
4. 使用统一的出站代理,由代理完成 DNS 解析和网络限制
5. 在网络层禁止访问内网地址,作为最终兜底单纯应用层校验很难彻底解决 DNS Rebinding,因此网络层出站控制非常重要。
13. 第八层防护:禁用自动重定向
重定向是 SSRF 中非常常见的绕过方式。
例如用户输入:
https://normal-domain.com/resource这个地址本身可能是安全的,但它返回:
HTTP/1.1 302 Found
Location: http://127.0.0.1:8080/admin如果 HTTP 客户端自动跟随重定向,就可能绕过第一次 URL 校验。
正确做法:
默认禁用自动重定向
如确实需要重定向,则每一次 Location 都重新做完整 SSRF 校验
限制最大重定向次数例如:
最多允许 3 次跳转
每次跳转都校验 scheme、host、port、DNS、IP
跳转到非法地址立即终止14. 第九层防护:限制请求和响应
SSRF 防护不仅要校验目标地址,还要限制请求行为本身。
建议设置:
连接超时时间
读取超时时间
最大响应体大小
允许的 Content-Type
最大下载文件大小
最大重定向次数
禁止携带敏感 Header
禁止携带服务端 Cookie
禁止返回完整错误堆栈例如:
<span class="token key atrule">httpClient</span><span class="token punctuation">:</span>
<span class="token key atrule">connectTimeoutMs</span><span class="token punctuation">:</span> <span class="token number">3000</span>
<span class="token key atrule">readTimeoutMs</span><span class="token punctuation">:</span> <span class="token number">10000</span>
<span class="token key atrule">maxResponseSizeMb</span><span class="token punctuation">:</span> <span class="token number">10</span>
<span class="token key atrule">maxRedirects</span><span class="token punctuation">:</span> <span class="token number">0</span>尤其要注意:不要把服务端请求到的原始响应直接返回给用户。
错误做法:
请求目标 URL
↓
把完整响应头、响应体、错误信息全部返回给前端更安全的方式:
只返回业务需要的字段
过滤响应头
限制响应体大小
错误信息脱敏
不暴露内部请求细节15. 第十层防护:云环境中的元数据服务保护
云环境中的 SSRF 风险尤其高。
很多云服务器都提供元数据服务,用于让实例获取自身信息、临时凭证、角色权限等。攻击者如果通过 SSRF 访问到元数据服务,可能进一步获取云资源访问权限。
防护建议:
启用更安全的元数据访问机制
禁用旧版不安全访问方式
限制应用访问元数据地址
服务账号使用最小权限
不要给普通业务服务绑定高权限角色
容器网络层禁止访问 metadata endpoint以 AWS 为例,应优先使用 IMDSv2,并限制不必要的元数据访问能力。
同时要强调一点:不要只依赖云厂商元数据服务自身的保护机制。应用层、网络层、权限层都应该同时做约束。
16. 第十一层防护:网络层出站控制
SSRF 最可靠的防护不是“每个开发都把 URL 校验写对”,而是:
即使代码层有遗漏,服务本身也访问不到不该访问的地址。
这就需要网络层出站控制。
推荐做法:
业务服务不能任意访问内网
外部请求必须走统一 Egress Proxy
防火墙 / 安全组限制出站流量
Kubernetes 使用 NetworkPolicy 控制 Pod 出站访问
禁止访问云元数据地址
禁止访问数据库、缓存、配置中心、容器管理接口理想结构:
业务服务
↓
统一出站代理
↓
公网目标地址统一出站代理负责:
域名白名单
IP 黑名单
CIDR 拦截
DNS 解析
重定向校验
访问日志
限流
告警这样可以把安全策略集中在基础设施层,而不是依赖每一个业务服务自己实现。
17. Java 中的 SafeUrlValidator 示例
下面是一个简化版示例,用于说明基本思路。
注意:真实生产环境还需要补充完整 CIDR 判断、IPv6 处理、重定向校验、DNS Rebinding 防护和网络层隔离。
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">java<span class="token punctuation">.</span>net<span class="token punctuation">.</span></span><span class="token operator">*</span></span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token import"><span class="token namespace">java<span class="token punctuation">.</span>util<span class="token punctuation">.</span></span><span class="token class-name">Set</span></span><span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SafeUrlValidator</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">String</span><span class="token punctuation">></span></span> <span class="token constant">ALLOWED_SCHEMES</span> <span class="token operator">=</span> <span class="token class-name">Set</span><span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token string">"http"</span><span class="token punctuation">,</span> <span class="token string">"https"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Integer</span><span class="token punctuation">></span></span> <span class="token constant">ALLOWED_PORTS</span> <span class="token operator">=</span> <span class="token class-name">Set</span><span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span><span class="token number">80</span><span class="token punctuation">,</span> <span class="token number">443</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">validate</span><span class="token punctuation">(</span><span class="token class-name">String</span> rawUrl<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token class-name">URI</span> uri<span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{<!-- --></span>
uri <span class="token operator">=</span> <span class="token constant">URI</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>rawUrl<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">normalize</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Invalid URL"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">String</span> scheme <span class="token operator">=</span> uri<span class="token punctuation">.</span><span class="token function">getScheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>scheme <span class="token operator">==</span> <span class="token keyword">null</span> <span class="token operator">||</span> <span class="token operator">!</span><span class="token constant">ALLOWED_SCHEMES</span><span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>scheme<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Unsupported URL scheme"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">String</span> host <span class="token operator">=</span> uri<span class="token punctuation">.</span><span class="token function">getHost</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>host <span class="token operator">==</span> <span class="token keyword">null</span> <span class="token operator">||</span> host<span class="token punctuation">.</span><span class="token function">isBlank</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Invalid host"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">int</span> port <span class="token operator">=</span> uri<span class="token punctuation">.</span><span class="token function">getPort</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>port <span class="token operator">==</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
port <span class="token operator">=</span> <span class="token string">"https"</span><span class="token punctuation">.</span><span class="token function">equalsIgnoreCase</span><span class="token punctuation">(</span>scheme<span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token number">443</span> <span class="token operator">:</span> <span class="token number">80</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token constant">ALLOWED_PORTS</span><span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>port<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Port not allowed"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token class-name">InetAddress</span><span class="token punctuation">[</span><span class="token punctuation">]</span> addresses<span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{<!-- --></span>
addresses <span class="token operator">=</span> <span class="token class-name">InetAddress</span><span class="token punctuation">.</span><span class="token function">getAllByName</span><span class="token punctuation">(</span>host<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">UnknownHostException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Cannot resolve host"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token class-name">InetAddress</span> address <span class="token operator">:</span> addresses<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">isBlockedAddress</span><span class="token punctuation">(</span>address<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"Blocked target address"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">private</span> <span class="token keyword">boolean</span> <span class="token function">isBlockedAddress</span><span class="token punctuation">(</span><span class="token class-name">InetAddress</span> address<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> address<span class="token punctuation">.</span><span class="token function">isAnyLocalAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token operator">||</span> address<span class="token punctuation">.</span><span class="token function">isLoopbackAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token operator">||</span> address<span class="token punctuation">.</span><span class="token function">isLinkLocalAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token operator">||</span> address<span class="token punctuation">.</span><span class="token function">isMulticastAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token operator">||</span> address<span class="token punctuation">.</span><span class="token function">isSiteLocalAddress</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>这个示例只能作为基础版本,不能直接视为完整生产方案。
更完整的生产实现应该包括:
完整 CIDR 网段判断
IPv4 / IPv6 全覆盖
IDN / Punycode 规范化
禁止 userinfo
禁止异常端口
DNS 解析结果缓存策略
重定向逐跳校验
请求前和请求时一致性控制
统一出站代理
网络层兜底拦截18. HTTP 客户端也要统一封装
除了 URL 校验器,还应该封装统一的 HTTP 请求客户端。
示例逻辑:
SafeHttpClient.fetch(url)
↓
SafeUrlValidator.validate(url)
↓
禁用自动重定向
↓
设置超时
↓
设置最大响应体
↓
过滤请求头
↓
发起请求
↓
校验响应
↓
返回安全结果不要让业务代码直接使用:
RestTemplate
WebClient
OkHttpClient
HttpClient
URLConnection而应该统一走:
SafeHttpClient这样才能保证新增功能不会绕过 SSRF 防护。
19. 常见错误做法总结
错误 1:只用字符串 contains 判断
<span class="token keyword">if</span> <span class="token punctuation">(</span>url<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">"127.0.0.1"</span><span class="token punctuation">)</span> <span class="token operator">||</span> url<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">"localhost"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">RuntimeException</span><span class="token punctuation">(</span><span class="token string">"blocked"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>这种做法很容易被绕过。
错误 2:只校验用户输入的原始 URL
原始 URL 安全,不代表最终请求地址安全。
还需要考虑:
DNS 解析
CNAME
重定向
URL 编码
IPv6
特殊 IP 表示法错误 3:自动跟随重定向
自动重定向可能把请求带到内网地址。
建议默认禁用,如需启用,必须逐跳校验。
错误 4:允许任意端口
如果允许访问任意端口,攻击者可能探测或访问内部管理端口。
建议默认只允许 80 和 443。
错误 5:把响应原样返回给用户
如果目标地址是内部接口,原样返回响应可能泄露配置、密钥、环境变量、服务状态等敏感信息。
错误 6:只做应用层防护,没有网络层兜底
应用层校验总有遗漏风险。
真正可靠的方案一定要配合:
防火墙
安全组
NetworkPolicy
Egress Proxy
云权限最小化20. 一套可落地的 SSRF 防护清单
输入校验
限制 URL 长度
使用标准 URL parser
只允许 http / https
禁止 userinfo
禁止异常 schema
规范化 host目标校验
优先使用域名白名单
限制端口
解析 DNS
校验所有 A / AAAA 记录
禁止内网地址
禁止 link-local 地址
禁止 metadata 地址
禁止 IPv6 内网地址请求控制
统一 SafeHttpClient
禁用自动重定向
逐跳校验 Location
设置连接超时
设置读取超时
限制响应体大小
限制 Content-Type
禁止携带敏感 Header响应处理
不要原样返回响应
过滤响应头
错误信息脱敏
限制返回字段
记录请求摘要网络隔离
业务服务出站最小化
统一 Egress Proxy
禁止访问内网网段
禁止访问云元数据服务
禁止访问数据库、缓存、配置中心
容器和 Pod 使用最小网络权限云环境
启用安全版本的元数据服务
禁用不必要的元数据访问
服务账号最小权限
不要给普通应用绑定高权限角色
在网络层阻断 metadata endpoint日志审计
记录请求目标 host
记录解析后的 IP
记录端口
记录拦截原因
记录重定向链路
对访问内网地址的尝试做告警21. 推荐的配置示例
<span class="token key atrule">ssrf</span><span class="token punctuation">:</span>
<span class="token key atrule">allowedSchemes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> http
<span class="token punctuation">-</span> https
<span class="token key atrule">allowedPorts</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token number">80</span>
<span class="token punctuation">-</span> <span class="token number">443</span>
<span class="token key atrule">blockedCidrs</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> 127.0.0.0/8
<span class="token punctuation">-</span> 10.0.0.0/8
<span class="token punctuation">-</span> 172.16.0.0/12
<span class="token punctuation">-</span> 192.168.0.0/16
<span class="token punctuation">-</span> 169.254.0.0/16
<span class="token punctuation">-</span> 0.0.0.0/8
<span class="token punctuation">-</span> 100.64.0.0/10
<span class="token punctuation">-</span> 224.0.0.0/4
<span class="token punctuation">-</span> 240.0.0.0/4
<span class="token punctuation">-</span> <span class="token punctuation">:</span><span class="token punctuation">:</span>1/128
<span class="token punctuation">-</span> fc00<span class="token punctuation">:</span><span class="token punctuation">:</span>/7
<span class="token punctuation">-</span> fe80<span class="token punctuation">:</span><span class="token punctuation">:</span>/10
<span class="token key atrule">redirect</span><span class="token punctuation">:</span>
<span class="token key atrule">enabled</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
<span class="token key atrule">maxHops</span><span class="token punctuation">:</span> <span class="token number">0</span>
<span class="token key atrule">response</span><span class="token punctuation">:</span>
<span class="token key atrule">maxBodySizeMb</span><span class="token punctuation">:</span> <span class="token number">10</span>
<span class="token key atrule">connectTimeoutMs</span><span class="token punctuation">:</span> <span class="token number">3000</span>
<span class="token key atrule">readTimeoutMs</span><span class="token punctuation">:</span> <span class="token number">10000</span>如果业务必须支持重定向,可以改成:
<span class="token key atrule">redirect</span><span class="token punctuation">:</span>
<span class="token key atrule">enabled</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">maxHops</span><span class="token punctuation">:</span> <span class="token number">3</span>
<span class="token key atrule">validateEachHop</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>22. SSRF 防护的核心结论
SSRF 看起来只是“服务端请求了一个 URL”,但本质上是服务端网络边界被用户输入控制的问题。
防护 SSRF 不能只靠一个简单的黑名单判断,而应该建立完整的安全链路:
统一请求入口
+
URL 解析与规范化
+
协议、端口、域名校验
+
DNS 解析结果校验
+
重定向逐跳校验
+
响应限制
+
网络层出站控制
+
云环境权限最小化一句话总结:
SSRF 防护的关键,不是判断 URL 里有没有 localhost,而是不要让服务端拥有不受约束的网络访问能力。
在工程实践中,最推荐的方案是:
白名单优先
统一 SafeHttpClient
禁用默认重定向
DNS + IP 双重校验
Egress Proxy 统一出站
网络层禁止访问内网和云元数据只有应用层和基础设施层一起做防护,才能真正降低 SSRF 风险。
写这篇文章的目的,不是为了证明某个技术有多先进,而是希望把一个真实问题拆开,讲清楚它背后的设计逻辑、工程取舍和落地路径。
AI 应用开发正在从“会调用模型”进入“会设计系统”的阶段。
模型只是起点,真正决定效果的,往往是数据、流程、工具、上下文、检索、评估、工程架构,以及持续迭代的能力。
我会继续围绕这些方向做系统化分享:
Agent、RAG、LLM 工程化、企业 AI 应用、私有化部署、系统架构与真实项目复盘。
希望这里不仅是一个技术博客,也能逐渐成为一个聚集 AI 应用开发者、产品实践者和工程落地者的交流空间。
dd-y 的技术博客:把想法落地,把技术讲透。
欢迎关注,一起把 AI 从 Demo 做到真正可用。
如果你也在关注 AI 应用落地、Agent 开发、RAG 系统、LLM 工程化、企业知识库、私有化部署 等方向,欢迎扫码加入我的技术交流群。
这里不会只聊概念,更希望一起交流真实项目中的问题、方案、踩坑经验和落地思路。
备注:如果二维码过期,可以私信我拉你进群。