Skip to content
阅读进度0%
知识库待复核

一个 URL 参数,为什么可能打穿你的内网?

CSDN 原文全文镜像:SSRF,全称是 Server-Side Request Forgery,中文通常叫“服务端请求伪造”。它的核心问题是:攻击者无法直接访问某个目标资源,但可以诱导服务端替他访问。用户输入 URL↓服务端请求该 URL↓服务端把内容返回给……

CSDN 原文镜像

本文为作者 CSDN 博客的全文镜像,原文发布于 2026-06-10。为适配本站结构,仅补充了站内元数据与来源说明,正文主体保持原文内容。

在这里插入图片描述

一文讲清 SSRF:从漏洞原理到工程化防护方案

在 Web 安全中,SSRF 是一个非常典型、也非常容易被低估的漏洞。

很多开发者第一次接触 SSRF 时,会觉得它只是“后端请求了一个用户传入的 URL”。但在真实系统中,SSRF 的影响远不止如此。它可能被用来访问内网服务、探测端口、读取云厂商元数据、绕过防火墙,甚至进一步扩大为权限泄露和横向移动。

本文会从工程实践角度,系统介绍 SSRF 的基本原理、常见攻击面、防护难点,以及一套相对完整的服务端防护方案。


1. 什么是 SSRF?

SSRF,全称是 Server-Side Request Forgery,中文通常叫“服务端请求伪造”。

它的核心问题是:

攻击者无法直接访问某个目标资源,但可以诱导服务端替他访问。

举个简单例子,假设系统提供了一个“根据 URL 抓取网页内容”的功能:

text
用户输入 URL

服务端请求该 URL

服务端把内容返回给用户

正常情况下,用户可能输入:

text
https://example.com/article/123

但攻击者可能输入:

text
http://127.0.0.1:8080/admin
http://localhost:6379
http://169.254.169.254/latest/meta-data/

如果后端没有任何限制,就会变成:

text
攻击者

业务服务器

内网服务 / 本机端口 / 云元数据服务

这就是 SSRF 的本质:攻击者控制了服务端请求的目标地址。


2. SSRF 为什么危险?

SSRF 危险的原因在于,服务端通常比外部用户拥有更高的网络权限。

外部攻击者可能无法访问内网服务,但业务服务器可以。攻击者只要能让服务器发起请求,就可能借助服务器的身份进入本来无法触达的网络区域。

常见风险包括:

风险类型说明
访问本机服务127.0.0.1localhost 上的管理接口
访问内网服务如数据库、Redis、Nacos、Elasticsearch、Prometheus 等
探测内网端口通过响应差异判断内网服务是否存在
访问云元数据服务获取实例信息、临时凭证、角色权限等敏感数据
绕过网络边界借助服务器突破防火墙、VPN、ACL 等限制
敏感信息泄露返回配置、环境变量、Token、密钥等
扩大攻击面作为进一步攻击内网系统的跳板

SSRF 的麻烦之处在于:它不是单纯的输入校验问题,而是涉及 URL 解析、DNS、HTTP 重定向、网络隔离、云环境权限等多个层面。


3. 哪些功能容易产生 SSRF?

只要系统存在“服务端根据用户可控输入去请求外部资源”的逻辑,就需要考虑 SSRF。

常见场景包括:

3.1 URL 内容抓取

例如:

text
输入文章链接,系统自动抓取正文
输入图片链接,系统自动下载图片
输入网页地址,系统自动生成预览
输入远程文件地址,系统自动导入文件

这类功能是 SSRF 的高发区域。


3.2 Webhook / Callback

很多系统允许用户配置回调地址:

text
任务完成后,请求用户配置的 callbackUrl

如果没有限制,攻击者可以把 callbackUrl 配成内网地址,让服务器主动访问内部系统。


3.3 图片代理和文件代理

有些系统为了避免前端跨域,会提供图片代理功能:

text
/image-proxy?url=https://example.com/a.png

后端读取 url 参数,再把图片内容返回给前端。

这个功能如果不做校验,很容易被改成:

text
/image-proxy?url=http://127.0.0.1:8080/admin

3.4 HTML 转 PDF / Markdown 渲染 / 富文本解析

这类场景经常被忽略。

例如系统支持 HTML 转 PDF,HTML 中可能包含:

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 防护为什么不能只靠黑名单?

很多人第一反应是:

text
禁止 127.0.0.1
禁止 localhost
禁止 169.254.169.254

这当然有用,但远远不够。

因为 SSRF 可以通过很多方式绕过简单黑名单,例如:

text
使用域名解析到内网 IP
使用 IPv6 地址
使用特殊 IP 写法
使用短地址、整数地址、八进制地址
使用 DNS Rebinding
使用 302 重定向跳转到内网
使用 CNAME 指向内网地址
使用大小写、编码、URL 解析差异绕过

例如你禁止了:

text
127.0.0.1

但攻击者可能使用:

text
localhost
127.1
0.0.0.0
[::1]
某个解析到 127.0.0.1 的域名

所以 SSRF 防护的核心不是“写几个黑名单字符串”,而是构建一整套请求安全边界。


5. SSRF 防护的总体思路

更合理的 SSRF 防护应该是多层防御:

text
用户输入

URL 解析与规范化

协议校验

域名 / IP 校验

端口校验

DNS 解析结果校验

重定向校验

统一安全 HTTP 客户端

网络层出站控制

日志审计与告警

核心原则有三点:

text
1. 不要信任用户传入的 URL
2. 不要让业务服务拥有任意访问网络的能力
3. 不要只依赖代码校验,必须配合网络层隔离

6. 第一层防护:统一收口外部请求

最危险的写法是业务代码中到处直接请求用户传入的 URL。

错误示例:

java
<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>

这样的问题是:

text
任何业务代码都可能绕过安全检查
安全规则分散在多个地方
后续难以审计
新增功能容易再次引入 SSRF

更推荐的方式是封装统一组件:

text
SafeUrlValidator
SafeHttpClient
ExternalFetchService
WebhookDispatcher
ImageProxyService

所有服务端请求外部 URL 的逻辑,都必须走统一的安全组件。

推荐结构:

text
业务代码

SafeUrlValidator 校验 URL

SafeHttpClient 发起请求

Egress Proxy / 网络出口控制

目标站点

这样可以把 SSRF 防护逻辑集中到一个地方维护,而不是散落在各个业务模块中。


7. 第二层防护:优先使用白名单

SSRF 防护中,白名单通常比黑名单更可靠。

7.1 强白名单

如果业务场景比较明确,最推荐的方式是只允许访问指定域名。

例如:

yaml
<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

校验逻辑:

text
用户输入 URL

解析 host

判断 host 是否在 allowedDomains 中

不在白名单则拒绝

强白名单适合这些场景:

text
固定第三方接口调用
固定资源下载源
固定合作方域名
内部配置好的可信站点

这种方式安全性最高,维护成本也相对可控。


7.2 弱白名单 + 内网地址拦截

如果业务必须支持用户输入任意公网 URL,就不能只依赖域名白名单。这时至少要做:

text
只允许 http / https
禁止访问内网 IP
禁止访问本机地址
禁止访问云元数据地址
禁止访问特殊网段
限制端口
限制重定向
限制响应大小
限制超时时间

但需要注意:支持“任意公网 URL”本身就意味着更大的风险,因此更应该配合网络层隔离。


8. 第三层防护:限制 URL 协议

服务端请求外部资源时,通常只应该允许:

text
http
https

应该禁止:

text
file://
ftp://
gopher://
dict://
ldap://
jar://
netdoc://
data:
mailto:

尤其是 file:// 这类协议,可能导致读取本地文件。

示例校验逻辑:

java
<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>

不要用简单的字符串判断,例如:

java
<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. 第四层防护:限制端口

如果没有特殊业务需求,建议只允许:

text
80
443

危险端口包括:

text
22      SSH
2375    Docker API
3306    MySQL
5432    PostgreSQL
6379    Redis
9200    Elasticsearch
11211   Memcached
8848    Nacos
9000    对象存储服务
9001    管理控制台
10250   Kubelet

推荐配置:

yaml
<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>

处理默认端口时也要注意:

text
http 默认端口是 80
https 默认端口是 443

例如:

java
<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. 第五层防护:禁止访问内网和特殊地址

服务端应该禁止访问以下类型的地址:

text
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

尤其要注意:

text
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.x

169.254.169.254 是云环境中特别敏感的地址,很多云厂商的实例元数据服务都和这个地址有关。一旦被 SSRF 访问,可能造成云资源凭证或实例信息泄露。


11. 第六层防护:DNS 解析结果校验

很多系统只检查 URL 字符串,这是不够的。

攻击者可以使用一个看起来正常的域名,但该域名解析到内网地址。

例如:

text
http://safe-looking-domain.com

解析结果可能是:

text
127.0.0.1
10.0.0.10
192.168.1.100

因此,正确流程应该是:

text
解析域名

获取所有 A / AAAA 记录

逐个判断是否属于内网或特殊地址

只要有一个非法地址,就拒绝

Java 示例:

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 防护中的一个难点。

它的大致思路是:

text
第一次解析域名时,返回公网 IP
安全校验通过

实际请求或后续请求时,域名变成内网 IP
绕过校验

这类问题本质上属于 TOCTOU 问题:

text
Time Of Check
检查时是安全的

Time Of Use
使用时已经变了

防护建议:

text
1. 请求前解析域名,并校验所有解析结果
2. 不要只在任务创建时校验一次,真正请求前也要校验
3. 每次重定向后都重新解析和校验
4. 使用统一的出站代理,由代理完成 DNS 解析和网络限制
5. 在网络层禁止访问内网地址,作为最终兜底

单纯应用层校验很难彻底解决 DNS Rebinding,因此网络层出站控制非常重要。


13. 第八层防护:禁用自动重定向

重定向是 SSRF 中非常常见的绕过方式。

例如用户输入:

text
https://normal-domain.com/resource

这个地址本身可能是安全的,但它返回:

http
HTTP/1.1 302 Found
Location: http://127.0.0.1:8080/admin

如果 HTTP 客户端自动跟随重定向,就可能绕过第一次 URL 校验。

正确做法:

text
默认禁用自动重定向
如确实需要重定向,则每一次 Location 都重新做完整 SSRF 校验
限制最大重定向次数

例如:

text
最多允许 3 次跳转
每次跳转都校验 scheme、host、port、DNS、IP
跳转到非法地址立即终止

14. 第九层防护:限制请求和响应

SSRF 防护不仅要校验目标地址,还要限制请求行为本身。

建议设置:

text
连接超时时间
读取超时时间
最大响应体大小
允许的 Content-Type
最大下载文件大小
最大重定向次数
禁止携带敏感 Header
禁止携带服务端 Cookie
禁止返回完整错误堆栈

例如:

yaml
<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>

尤其要注意:不要把服务端请求到的原始响应直接返回给用户。

错误做法:

text
请求目标 URL

把完整响应头、响应体、错误信息全部返回给前端

更安全的方式:

text
只返回业务需要的字段
过滤响应头
限制响应体大小
错误信息脱敏
不暴露内部请求细节

15. 第十层防护:云环境中的元数据服务保护

云环境中的 SSRF 风险尤其高。

很多云服务器都提供元数据服务,用于让实例获取自身信息、临时凭证、角色权限等。攻击者如果通过 SSRF 访问到元数据服务,可能进一步获取云资源访问权限。

防护建议:

text
启用更安全的元数据访问机制
禁用旧版不安全访问方式
限制应用访问元数据地址
服务账号使用最小权限
不要给普通业务服务绑定高权限角色
容器网络层禁止访问 metadata endpoint

以 AWS 为例,应优先使用 IMDSv2,并限制不必要的元数据访问能力。

同时要强调一点:不要只依赖云厂商元数据服务自身的保护机制。应用层、网络层、权限层都应该同时做约束。


16. 第十一层防护:网络层出站控制

SSRF 最可靠的防护不是“每个开发都把 URL 校验写对”,而是:

即使代码层有遗漏,服务本身也访问不到不该访问的地址。

这就需要网络层出站控制。

推荐做法:

text
业务服务不能任意访问内网
外部请求必须走统一 Egress Proxy
防火墙 / 安全组限制出站流量
Kubernetes 使用 NetworkPolicy 控制 Pod 出站访问
禁止访问云元数据地址
禁止访问数据库、缓存、配置中心、容器管理接口

理想结构:

text
业务服务

统一出站代理

公网目标地址

统一出站代理负责:

text
域名白名单
IP 黑名单
CIDR 拦截
DNS 解析
重定向校验
访问日志
限流
告警

这样可以把安全策略集中在基础设施层,而不是依赖每一个业务服务自己实现。


17. Java 中的 SafeUrlValidator 示例

下面是一个简化版示例,用于说明基本思路。

注意:真实生产环境还需要补充完整 CIDR 判断、IPv6 处理、重定向校验、DNS Rebinding 防护和网络层隔离。

java
<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>

这个示例只能作为基础版本,不能直接视为完整生产方案。

更完整的生产实现应该包括:

text
完整 CIDR 网段判断
IPv4 / IPv6 全覆盖
IDN / Punycode 规范化
禁止 userinfo
禁止异常端口
DNS 解析结果缓存策略
重定向逐跳校验
请求前和请求时一致性控制
统一出站代理
网络层兜底拦截

18. HTTP 客户端也要统一封装

除了 URL 校验器,还应该封装统一的 HTTP 请求客户端。

示例逻辑:

text
SafeHttpClient.fetch(url)

SafeUrlValidator.validate(url)

禁用自动重定向

设置超时

设置最大响应体

过滤请求头

发起请求

校验响应

返回安全结果

不要让业务代码直接使用:

text
RestTemplate
WebClient
OkHttpClient
HttpClient
URLConnection

而应该统一走:

text
SafeHttpClient

这样才能保证新增功能不会绕过 SSRF 防护。


19. 常见错误做法总结

错误 1:只用字符串 contains 判断

java
<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 安全,不代表最终请求地址安全。

还需要考虑:

text
DNS 解析
CNAME
重定向
URL 编码
IPv6
特殊 IP 表示法

错误 3:自动跟随重定向

自动重定向可能把请求带到内网地址。

建议默认禁用,如需启用,必须逐跳校验。


错误 4:允许任意端口

如果允许访问任意端口,攻击者可能探测或访问内部管理端口。

建议默认只允许 80 和 443。


错误 5:把响应原样返回给用户

如果目标地址是内部接口,原样返回响应可能泄露配置、密钥、环境变量、服务状态等敏感信息。


错误 6:只做应用层防护,没有网络层兜底

应用层校验总有遗漏风险。

真正可靠的方案一定要配合:

text
防火墙
安全组
NetworkPolicy
Egress Proxy
云权限最小化

20. 一套可落地的 SSRF 防护清单

输入校验

text
限制 URL 长度
使用标准 URL parser
只允许 http / https
禁止 userinfo
禁止异常 schema
规范化 host

目标校验

text
优先使用域名白名单
限制端口
解析 DNS
校验所有 A / AAAA 记录
禁止内网地址
禁止 link-local 地址
禁止 metadata 地址
禁止 IPv6 内网地址

请求控制

text
统一 SafeHttpClient
禁用自动重定向
逐跳校验 Location
设置连接超时
设置读取超时
限制响应体大小
限制 Content-Type
禁止携带敏感 Header

响应处理

text
不要原样返回响应
过滤响应头
错误信息脱敏
限制返回字段
记录请求摘要

网络隔离

text
业务服务出站最小化
统一 Egress Proxy
禁止访问内网网段
禁止访问云元数据服务
禁止访问数据库、缓存、配置中心
容器和 Pod 使用最小网络权限

云环境

text
启用安全版本的元数据服务
禁用不必要的元数据访问
服务账号最小权限
不要给普通应用绑定高权限角色
在网络层阻断 metadata endpoint

日志审计

text
记录请求目标 host
记录解析后的 IP
记录端口
记录拦截原因
记录重定向链路
对访问内网地址的尝试做告警

21. 推荐的配置示例

yaml
<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>

如果业务必须支持重定向,可以改成:

yaml
<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 不能只靠一个简单的黑名单判断,而应该建立完整的安全链路:

text
统一请求入口
+
URL 解析与规范化
+
协议、端口、域名校验
+
DNS 解析结果校验
+
重定向逐跳校验
+
响应限制
+
网络层出站控制
+
云环境权限最小化

一句话总结:

SSRF 防护的关键,不是判断 URL 里有没有 localhost,而是不要让服务端拥有不受约束的网络访问能力。

在工程实践中,最推荐的方案是:

text
白名单优先
统一 SafeHttpClient
禁用默认重定向
DNS + IP 双重校验
Egress Proxy 统一出站
网络层禁止访问内网和云元数据

只有应用层和基础设施层一起做防护,才能真正降低 SSRF 风险。

写这篇文章的目的,不是为了证明某个技术有多先进,而是希望把一个真实问题拆开,讲清楚它背后的设计逻辑、工程取舍和落地路径。

AI 应用开发正在从“会调用模型”进入“会设计系统”的阶段。

模型只是起点,真正决定效果的,往往是数据、流程、工具、上下文、检索、评估、工程架构,以及持续迭代的能力。

我会继续围绕这些方向做系统化分享:

Agent、RAG、LLM 工程化、企业 AI 应用、私有化部署、系统架构与真实项目复盘。

希望这里不仅是一个技术博客,也能逐渐成为一个聚集 AI 应用开发者、产品实践者和工程落地者的交流空间。

dd-y 的技术博客:把想法落地,把技术讲透。

欢迎关注,一起把 AI 从 Demo 做到真正可用。


如果你也在关注 AI 应用落地、Agent 开发、RAG 系统、LLM 工程化、企业知识库、私有化部署 等方向,欢迎扫码加入我的技术交流群。

这里不会只聊概念,更希望一起交流真实项目中的问题、方案、踩坑经验和落地思路。
在这里插入图片描述

备注:如果二维码过期,可以私信我拉你进群。

基于 VitePress 构建