先说一个我自己的经历。某次线上排查,发现一台业务服务器在半夜不断向内网发起非常规的请求,抓包一看,目标端口从 3306 扫到 6379,再扫到 11211。顺着调用链往上追,问题落在了我们自己做的一个“URL预览”功能上——前端传一个链接,后端为了生成预览信息,直接拿这个链接去请求。这其实就是一场典型的服务端请求伪造,也就是 Security 圈里常说的 SSRF。
SSRF 的全称是 Server-Side Request Forgery,中文一般叫“服务端请求伪造”。很多刚入门的朋友一听这个名字就觉得高深,其实它表达的意思非常直白:服务端接收了一个攻击者可控的 URL,然后以服务端自己的身份去请求这个地址。关键在于“服务端发起的请求”,而不是用户的浏览器发起的请求。浏览器发出的请求只能访问公网资源,但服务端发出的请求,往往可以够到内网里那些根本不对外暴露的主机和服务。
这篇内容我会从零开始拆这个漏洞。适合三类人看:一是刚接触 Web 安全、想系统理解漏洞原理的初学者;二是写后端接口、担心自己功能被利用的开发者;三是做安全测试或线上排查、需要快速定位问题的人。整篇不涉及高深数学,也不需要你提前掌握多少协议知识,只要你写过接口、调过接口就能看懂。
1. SSRF 的成因拆解:服务端为什么会“帮”攻击者发请求
1.1 从一次正常请求说起
要想理解 SSRF,先得弄明白普通网络请求是怎么流动的。假设你在浏览器里访问一个公开页面,请求是你发出去的,服务器收到后把页面返给你。整个过程里,发起请求的是你的浏览器终端,IP 是你的 IP,服务器只负责响应。
现在换个场景。你写了一个后端接口,它要做一件事:从某个 URL 获取 JSON 数据,解析之后存到数据库。这时候你自己的服务器就变成了“请求者”。服务端代码里写了类似“去访问这个地址并读取内容”的逻辑,而这个地址可能是写死在配置里,也可能来自用户输入。
SSRF 之所以成立,需要有三个要素同时满足:
- 服务端代码中存在“主动发起网络请求”的逻辑;
- 目标地址由外部输入控制,而不是固定写在代码里;
- 服务端对输入的目标地址没有做足够严格的限制。
当这三个条件凑齐,攻击者输入一个内网地址(比如 192.168.x.x),服务端同样会去请求它。因为请求是从服务端发出的,内网会认为这是可信来源,于是把本不该暴露的数据或服务响应返回给服务端。这就是“伪造”的含义——伪造的是发送方身份,攻击者借了服务器的名义去探访内部资源。
1.2 漏洞的核心条件:输入可控加请求触发
很多初学者会把 SSRF 和普通的“URL 跳转”混淆。URL 跳转是服务端返回一个 302 让浏览器跳转到指定地址,跳转动作发生在浏览器端。SSRF 则是服务端自己拿着 URL 去请求,动作发生在服务端。两者最大的差别就是“谁在执行请求”。
从触发方式上看,SSRF 往往藏在一些看起来很普通的功能里。比如:
- 上传图片时填写一个图片 URL,前端展示前需要后台拉取;
- 提交一个分享链接,后端要抓取网页标题、描述、缩略图;
- Webhook 回调地址由用户填写,触发事件后服务端去回调;
- 服务器需要代理请求外部接口,而代理地址直接取参数值。
这些功能表面上是“为了用户方便”,但都共同指向一个后端行为:按用户提供的地址,由服务器去访问它。只要地址可控,内网就会暴露在这个请求半径之内。
1.3 最容易写出 SSRF 的代码长什么样
来看一段很典型的伪代码,用 PHP 的 file_get_contents 实现远程拉取内容:
// example: /fetch?url=https://example.com/robots.txt $url = $_GET['url']; // 用户可控 $content = file_get_contents($url); // 服务端直接请求 echo $content;这段代码的逻辑非常清晰:从参数取 url,请求它,然后把响应当作页面内容返回。功能上没毛病,可是攻击者只要把参数从https://example.com/robots.txt换成http://127.0.0.1:3306,服务端就会尝试连接本地数据库端口。如果换成内网某台主机,就相当于以服务端为跳板,向内网发起访问。
Python 场景也很常见,用 requests 库的话问题更隐蔽:
# example: /proxy?url=http://api.example.com/data from flask import request import requests def proxy(): target = request.args.get('url') resp = requests.get(target, timeout=5) return resp.text代码写得没问题,从功能角度它确实是个现成的代理。但这正是问题所在:任何用户可以来用,任何地址都可以填,目标端口、目标协议都不受限。
Java 里 HttpClient、Go 里的 http.Get,本质都一样。凡是把外部输入直接拼进请求目标的操作,基本都是高危写法。代码审计时看到这一类写法,就要打起精神问一句:这个输入真的可控吗?输入做了什么限制吗?即使没有直接回显,服务端发起请求本身也可能被用来探测端口、标识可用服务。
2. 业务场景与危害面:先知道哪些功能要重点留意
2.1 高危功能入口盘点
把所有可能出现 SSRF 的功能点列一遍,你在排查代码时就能按图索骥。我自己整理的高频区域有这些:
| 功能类型 | 典型实现 | 风险说明 |
|---|---|---|
| 图片加载/抓取 | 传入图片 URL 后服务器下载并转存 | 可探测内网 HTTP 服务、读取文件 |
| 链接预览/分享卡片 | 抓取标题、描述、封面 | 最常见的 SSRF 入口,开发最常忽略 |
| 文档导入/转换 | 通过 URL 导入文件再转 PDF 或图片 | 可配合跨协议扩展为读取本地文件 |
| Webhook 回调 | 用户填回调地址,后端事件触发时 POST 数据 | 地址完全可控,且能携带内部数据 |
| 接口代理/转发 | 后端转发请求到前端指定的地址 | 等于是送了一个可控代理 |
| 文件下载服务 | 用户填地址,后端下载到本地 | 内网文件甚至云元数据都可以访问 |
这些功能出现在后台管理、内容发布、用户个性化配置等场景里。尤其是后台管理系统,因为访问者少,开发时往往默认“公司内部人使用”,限制做得更松。可一旦攻击者通过其他漏洞进入后台,这些 SSRF 入口就成了横向移动的跳板。
2.2 SSRF 与 CSRF:经常被搞混的两个“请求伪造”
初学阶段容易把 SSRF 和 CSRF 放在一起记,因为他们名字里都有“RF”——Request Forgery。但两者的含义可以差出十万八千里。
| 维度 | SSRF | CSRF |
|---|---|---|
| 发起方 | 服务端 | 受害用户的浏览器 |
| 攻击者控制点 | URL/目标地址 | 伪造整个请求动作 |
| 主要影响 | 内网信息泄露、服务探测 | 以用户身份执行操作 |
| 请求携带的身份 | 服务端自身的权限 | 用户的登录态 |
| 典型场景 | 图片抓取、链接预览 | 更改密码、转账、改邮箱 |
一句话总结:CSRF 是“冒用用户身份”,SSRF 是“借用服务器能力”。两者都需要重点防护,但思考的方向完全不同。我在排查项目时,遇到“请求伪造”相关的问题,第一步都是先判断请求是发生在哪个上下文环境里,再判断该往哪个方向排查。
2.3 从请求者的角度理解影响面
理解 SSRF 的影响面,关键要理解一个概念:服务端发起请求时携带的是服务器自己的 IP 和权限。外网攻击者平时到不了内网,但服务器就生存在内网。攻击者能让服务器替他访问内网的任何地址,就等于给服务器装了一双“遥控的眼睛”。
这双眼睛能看到什么,取决于服务端放在哪、能拿到什么权限。如果服务器能访问数据库,那攻击者可以通过探测确认 3306 端口是否开放;如果服务器在云环境里,访问某个内部元数据地址可能拿到密钥和临时凭证;如果服务端能访问本地文件,跨协议读取可以让任意文件内容进入响应体。这些单看任何一个好像都还行,串起来就是一个完整的攻击链。
3. 利用视角下的深层危害
3.1 从一个端口扫描的角度理解探测行为
攻击者拿到一个 SSRF 漏洞后,第一件事往往不是直接打内网应用,而是先做信息收集。怎么收集?靠的是请求时间差和返回内容差异。
比如用 SSRF 访问http://127.0.0.1:8080和访问http://127.0.0.1:81,响应时间、报错提示、页面内容都会有细微区别。端口开放时连接建立得快,端口不通会超时或连接失败;有些服务会暴露 banner 信息,返回协议头部。攻击者把这些结果记录下来,就能在几十分钟之内摸清内网一段 IP 里哪些主机活着、哪些端口开放、运行着什么服务。
这种探测最大的特点是隐蔽。从攻击者的角度来看,请求的源 IP 是受攻击服务器本身的 IP,不是攻击者的 IP。内网日志里看到的是一台业务服务器在“正常地”访问其他内网主机,没有明显的异常终端标识。这比攻击者直接向内网发包要隐蔽得多。
3.2 跨协议请求:不只是 HTTP
SSRF 经常被误解为“只能探测内网的 HTTP 服务”。事实上,服务端发起的请求完全可以携带其他协议。PHP 的 file_get_contents/curl、Python 的 requests、Java 的 URLConnection,在开启允许协议的配置下,可以访问 file、gopher、dict、ftp 等协议。
这里我重点说两个协议,理解这两个协议的价值,就能理解跨协议攻击的严重性。
第一个是 file 协议。当后端允许 file 协议时,攻击者可以直接通过 URLfile:///etc/passwd读取服务器本地文件内容。如果响应内容会回显,那你等于送出了一个任意文件读取漏洞。即使不回显,攻击者也可以用盲读的方式探测文件的存在性。
第二个是 gopher 协议。gopher 协议的核心特点是允许客户端构造任意字节流发送给服务端,这就意味着攻击者可以“模拟”一次完整的 TCP 会话——比如向本地的 Redis 发送命令。虽然现代 Redis 默认配置已经不允许非认证访问,但在内网环境里仍存在配置不当的老实例。一旦打出这一步,就从网络层探测升级到了代码执行级攻击。
需要注意:不是所有后端语言都默认支持这些协议,很多拦截策略也能限制协议类型。但确认协议支持情况之前,攻击者只要多试几个前缀,就能知道后端到底放开了多少能力。防御者在设计时不能只盯着 HTTP,必须假设所有协议都可能被调用。
3.3 为什么这些危害对业务是致命的
把上面几点串起来,一个完整的 SSRF 攻击路径大约是这样的:先发现一个可控 URL 的接口,然后探测内网存活主机与端口,接着发现某个老的内部应用或数据库,再利用跨协议读取文件或写入命令,最后撬动整片内网。
这里面每一步都不需要极高深的技巧,真正困难的反而是“能不能发现第一个入口”。一旦入口被确认,后边的操作非常机械化。这也是为什么现在的一些安全评审会把 SSRF 的严重级别定得和 SQL 注入差不多——单看漏洞本身只是“能发请求”,但结合内网环境和业务逻辑,它可以演变成不同层级的真实入侵。
4. 防御设计:为什么很多限制方案会被绕过
4.1 常见的限制手段
说到 SSRF 防御,大家第一反应通常是“过滤内网 IP”。这个想法没错,但实现起来远比想象中复杂。常见的防护措施有:
- 限制请求协议只能是 HTTPS/HTTP;
- 对目标地址做主机名白名单校验;
- 对解析后的 IP 做内网地址黑名单限制;
- 禁止请求跳转,或限制跳转次数;
- 通过代理网关统一出网,业务层不直接发外部请求。
这些方案单独看都有道理,但组合起来才有效。真正的问题是:很多项目只做了其中一条,而且校验逻辑本身存在细节漏洞。下面重点说说这些“细节漏洞”长什么样。
4.2 绕过思路的技术根源
先说一个冷知识:同样是“127.0.0.1”,可以写成很多不同形式。比如十进制整数形式、十六进制形式、短横线形式、IPv6 映射形式。如果后端只做字符串过滤,判断“字符串中是否包含 192.168”,那攻击者只要换一种表示形式就绕过了。
解析差异也是一个大坑。有的方案在进入请求前先做一次域名解析,解析出 IP,然后拿这个 IP 做内网判断。但内部客户端请求时,往往会再做一次解析。如果两次解析的结果不一样——也就是所谓的 DNS 重绑定攻击——那你校验时看到的是公网 IP,真正请求时拿到的却是内网 IP。既然校验和实际执行用的是两次解析结果,那中间就是天然的绕过空间。
还有跳转处理。很多开发者想到了限制跳转,但方案是“只允许一次跳转”。攻击者构造一个公网地址,这个公网地址再 302 跳转到内网地址。第一次校验通过了,跳转后就没有第二次校验了。这种绕过实现起来非常简单,根本不用理解复杂原理,试一次就能成功。
再一个常见坑是 IP 判断不全面。只过滤了 IPv4 的内网地址段,但没过滤 IPv6 的内网地址段,或者没过滤本机回环地址的各种别名形式。攻击者的想象力远比防御者的规则表丰富,漏一种就相当于没防御。
4.3 一套相对可靠的防御方案
踩过不少坑之后,我现在比较推荐这套组合策略:
协议白名单:只允许 HTTP 和 HTTPS,其他协议一律当场拒绝。实现上用标准库解析 URL 的 scheme 部分,不要用简单的是否包含判断。
域名解析与 IP 多重校验:拿到目标地址后,先解析域名得到 IP 集合,对这个集合做内网地址判断。内网地址段要覆盖 IPv4 和 IPv6 的常见保留段,包括回环、链路本地、私有地址、保留地址等。
请求解析与实际请求拆分:DNS 解析结果只作为检查用,实际请求时用一个代理商层代理或网关,让网关再校验一次出口目标。两层都允许才放行,从根本上规避 DNS 重绑定。
禁止跳转:客户端库里的跟随跳转能力全部关闭。如果需要支持跳转,就在每一跳之后重新执行校验,绝不信任上一次的校验结果。
统一出网:构建一个独立代理或出网网关,业务服务器不直接访问外部,所有外联请求必须经过网关。网关配置目标白名单,未命中的请求直接拒绝。
这套方案不能说是绝对无法绕过,但它把需要攻击者同时利用的漏洞点提升到了多个。想绕过,得同时处理协议问题、解析差异、跳转处理再穿越网关,难度就不是单纯字符串过滤能比的。
5. 检测、排查与代码审计要点
5.1 日志侧怎么发现异常
平时做线上巡检,日志是最容易发现 SSRF 的入口。需要重点关注几类日志:
- 服务器对外发起请求的 IP 和端口记录,同一来源短时间内大量访问不同目标的端口,高度可疑;
- 带有 url、target、callback 等参数名的请求日志,如果访问目标里出现了内网地址段,基本可以判定存在问题;
- 业务日志中同一会话内出现高频连接失败记录,可能是在探测端口。
有一说一,纯日志检测对 SSRF 的覆盖效果有限,因为正常的业务外联也会产生大量日志。更靠谱的方式是网络侧监控做流量画像,或者直接做代码审计,从源头发现问题。日志更适合用于“发现了异常之后回溯攻击路径”,而不是作为第一道防线。
5.2 代码审计时要盯住的关键函数
代码审计时我一般从哪些函数看起?不同语言有不同的关注列表:
- PHP:file_get_contents、curl_exec、fopen、readfile
- Python:requests.get/post、urlopen、httpx 调用
- Java:HttpURLConnection、HttpClient.send、OkHttp 请求
- Go:http.Get、http.Post、net.Dial
只看关键函数还不够,还要追一层调用链。确认传入的参数来源、是否经过过滤、过滤发生在哪一层。有的项目在入口处做了全局过滤,那只需要检查过滤逻辑覆盖了哪些函数调用;有的项目只在部分路由里做了过滤,那其他路由全部是裸奔状态。
我习惯用“谁能控制这里面的参数”来作为出发点。如果参数来源是后端配置文件或环境变量,风险较低;如果参数来源于 GET/POST 参数、请求头或用户上传的内容,就要排查所有可能的校验路径。面试时很多人能背出 SSRF 的定义,但一问“你负责的项目里哪个接口存在这个风险”就卡住了。解决这个问题的唯一办法,就是顺着代码从数据入口一路跟到请求函数。
5.3 常见问题与排查技巧速查
最后整理一个我在日常交流中经常被问到的问题列表,以问答形式给出来,方便快速定位。
| 问题 | 判断思路 | 处理方向 |
|---|---|---|
| 为什么限制了内网 IP 还是会被打? | 可能是校验和解析不是同一份结果,也可能是 IP 格式变体或 IPv6 漏配 | 统一解析点,检查所有内网地址段 |
| 返回内容看不到任何内部信息,是不是就没危害? | 即使无回显也可用于端口探测和盲打 | 不能只依据回显判断风险 |
| 禁止了 http 协议就能解决吗? | 如果后端支持其他协议,提出了也未必堵死 | 协议白名单设置在解析层 |
| 服务端不支持时只能完全禁用外联吗? | 不建议禁用,业务往往需要外联 | 用专门的出网代理做白名单管控 |
| 开发测试环境能不能放开限制? | 测试环境的内网设备通常和线上共享 | 环境间安全等级建议保持一致 |
这几个问题对应的是实际场景里的高频误区。单独记结论不难,难的是遇到具体的业务场景时综合判断。我自己的经验是:把校验逻辑抽象成一个独立的公共组件,而不是散落在每个接口里写一遍。散落的逻辑最容易出偏差,一个接口写一种过滤,最后总有漏网之鱼。
写了这么多,核心还是要回到“为什么”上。SSRF 的成因不复杂,复杂的是它对系统架构的牵动。真正见过一次服务器被当跳板扫内网,你就能理解为什么我在每次代码评审时都要追着问“这个 url 是从哪来的”。安全不是靠某一个神级方案,而是靠每一个细节都不给攻击者留可乘之机。希望这篇内容,能让你在遇到类似逻辑时多留一个心眼。