背景
SSRF(Server-Side Request Forgery)这几年在真实业务里出现频率一直不低,尤其是带有“URL 预览、图片抓取、Webhook、在线导入、PDF 渲染、远程文件同步”等能力的系统。它的问题不在于某个请求函数本身危险,而是服务端替用户访问了不可信 URL,并且访问环境往往比普通用户浏览器更敏感。
这篇文章整理一次较典型的 SSRF 排查思路,重点放在代码审计、复现验证和修复策略上。内容只讨论授权测试环境中的分析方法,不涉及未授权利用、内网探测或攻击扩展。
场景描述
某业务提供“远程图片导入”功能,用户提交一个图片 URL,后端下载图片并存入对象存储。功能看起来很常见,入口大概类似:
POST /api/image/import
Content-Type: application/json
{
"url": "
}后端逻辑包括三步:
- 校验 URL 字符串格式。
- 使用 HTTP 客户端请求该 URL。
- 判断响应 Content-Type 和文件大小后保存。
问题出在第一步和第二步之间:代码只判断了 URL 是否以 http 或 https 开头,没有对解析后的主机、重定向、DNS 解析结果做约束。
问题代码示例
下面是一个抽象后的示例,语言以 Java 为例,其他语言中的问题本质相同:
public byte[] fetchImage(String url) throws IOException {
if (!url.startsWith(" && !url.startsWith(" {
throw new IllegalArgumentException("invalid scheme");
}
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
conn.setInstanceFollowRedirects(true);
String contentType = conn.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("not image");
}
return conn.getInputStream().readAllBytes();
}这段代码有几个常见隐患:
- 只做字符串前缀判断,未使用标准 URL 解析结果进行校验。
- 默认跟随重定向,初始 URL 安全不代表跳转后的 URL 安全。
- 只校验 Content-Type,无法约束服务端实际访问目标。
- 没有限制解析出的 IP 范围,例如本地地址、内网地址、链路本地地址等。
- 没有响应体大小上限,存在资源消耗风险。
技术分析
SSRF 排查时,不建议只盯着“能不能访问某个地址”,更重要的是梳理服务端请求链路。一个安全的远程抓取功能,至少要明确以下几个点:
- 允许哪些协议:通常只允许 http 和 https。
- 允许访问哪些域名:优先白名单,而不是黑名单。
- 是否允许跳转:如果允许,跳转后的每一跳都要重新校验。
- DNS 解析结果是否可信:需要检查解析后的 IP 是否落在禁止访问的网段。
- 是否存在 DNS Rebinding:校验和实际连接之间不能完全割裂。
- 请求方法、请求头、超时、最大响应大小是否受控。
其中最容易被忽略的是重定向和 DNS。很多修复只检查用户输入的第一跳 URL,但 HTTP 客户端自动跟随 302 后,真正访问的可能已经不是最初校验过的目标。
安全复现思路
在授权测试环境中,可以搭建两个本地服务来模拟“正常外部地址跳转到受限地址”的情况。这里不提供针对真实内网资产的利用步骤,只用于验证业务代码是否会跟随跳转、是否对跳转目标重新校验。
# 启动一个普通测试服务,返回 302 跳转
python3 -m http.server 8000也可以用一个最小 Flask 服务模拟响应:
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def r():
return redirect(' code=302)
app.run(host='0.0.0.0', port=8000)另一个端口作为受限目标模拟服务:
from flask import Flask
app = Flask(__name__)
@app.route('/test')
def test():
return 'local service', 200, {'Content-Type': 'text/plain'}
app.run(host='127.0.0.1', port=9000)如果业务后端请求了第一个地址,并且第二个本地服务收到请求,就说明当前代码在重定向场景下存在访问边界失控问题。实际验证时应只在本机、测试容器或授权环境内进行。
关键审计点
代码审计时可以从“输入点”和“请求点”两个方向找。
1. 常见输入点
- 图片、头像、附件的远程导入。
- URL 预览、链接解析、网页截图。
- Webhook 回调地址配置。
- 第三方数据源导入。
- PDF/HTML 渲染中的远程资源加载。
- XML、Markdown、富文本解析中的外部资源引用。
2. 常见请求函数
- Java:HttpURLConnection、Apache HttpClient、OkHttp、RestTemplate、WebClient。
- Go:net/http、http.Client。
- Python:requests、urllib、aiohttp。
- Node.js:axios、node-fetch、request、got。
- PHP:curl、file_get_contents、Guzzle。
审计时建议全文检索这些请求库,再反向追踪 URL 参数来源。如果 URL 可被用户直接或间接控制,就需要继续确认是否有完整的目标校验。
修复思路
SSRF 的修复不要依赖单点判断,建议做成统一的安全请求组件,业务层禁止直接调用通用 HTTP 客户端访问用户传入 URL。
一个相对稳妥的策略如下:
- 只允许 http 和 https。
- 优先使用域名白名单,确实无法白名单时再做严格网段限制。
- 禁止访问本地地址、私有地址、链路本地地址、保留地址、组播地址等。
- 关闭自动重定向,手动处理跳转,并对每一跳重复校验。
- 限制最大跳转次数,例如不超过 3 次。
- 设置连接超时、读取超时、最大响应体大小。
- 限制请求方法和请求头,避免带入内部凭据。
- 下载类功能先落临时文件,再做类型和大小校验。
IP 范围校验示例
下面示例只展示思路,生产环境建议使用成熟 IP/CIDR 库,避免 IPv6、IPv4 映射地址、编码变体等细节处理不完整。
private static boolean isBlockedAddress(InetAddress address) {
return address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| address.isMulticastAddress();
}仅使用 isSiteLocalAddress 并不够,一些保留网段、特殊地址段、IPv6 场景还需要额外覆盖。实际工程里更建议维护一组禁止 CIDR,并统一做判断。
重定向处理示例
重点是不要让 HTTP 客户端自动跟随跳转,而是在每一跳前做 URL 和解析结果校验:
int maxRedirects = 3;
URL current = new URL(inputUrl);
for (int i = 0; i <= maxRedirects; i++) {
validateUrl(current);
HttpURLConnection conn = (HttpURLConnection) current.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
String location = conn.getHeaderField("Location");
if (location == null) {
throw new IOException("empty redirect location");
}
current = new URL(current, location);
continue;
}
if (code != 200) {
throw new IOException("unexpected status: " + code);
}
// 后续再做 Content-Type、大小和文件内容校验
break;
}这里的 validateUrl 应该包含 scheme 校验、host 校验、DNS 解析、IP 范围判断等逻辑。需要注意,DNS 校验和实际连接之间仍可能存在竞态风险,高安全要求场景可以考虑通过自定义 DNS 解析结果绑定连接目标,或者把远程抓取任务放到隔离网络环境中执行。
防护架构建议
如果业务确实需要频繁访问用户提供的外部 URL,仅靠代码层校验还不够,建议配合网络层隔离:
- 将抓取服务独立部署,避免与核心业务服务共用运行环境。
- 抓取服务所在网络禁止访问内部管理网段、元数据服务和数据库网段。
- 通过出口代理统一访问外网,并在代理层做域名、IP、端口策略。
- 对异常访问目标、失败率、响应大小做日志监控。
- 云环境中限制实例元数据服务访问,或使用 IMDSv2 等更安全配置。
工程上比较推荐“业务服务提交任务,隔离抓取服务执行,结果写入对象存储”的模式。这样即使抓取逻辑出现遗漏,影响面也会小很多。
日志与检测
排查历史风险时,可以从访问日志和应用日志里找一些特征:
- 用户提交的 URL host 为 IP 地址而非域名。
- URL 中出现 localhost、127.0.0.1、0.0.0.0、::1 等本地标识。
- 大量访问非标准端口。
- 短时间内同一账号提交大量不同 URL。
- 重定向链路异常,第一跳和最终访问 host 差异较大。
- 下载失败率明显高于正常用户。
日志中建议记录规范化后的 URL、最终访问 host、解析 IP、状态码、响应大小、耗时、重定向次数等字段。注意不要记录敏感响应体。
容易踩坑的点
- 只做字符串黑名单:URL 编码、大小写、IPv6、十进制/八进制 IP 表示都可能导致判断偏差。
- 只校验第一跳:自动重定向会绕过初始校验边界。
- 只看 Content-Type:Content-Type 是响应头,不代表访问目标安全。
- 忽略 IPv6:很多拦截规则只覆盖 IPv4。
- 允许任意端口:远程抓取通常只需要 80 和 443,其他端口应谨慎开放。
- 复用业务凭据:服务端请求外部地址时不应携带内部认证头或 Cookie。
总结
SSRF 的本质是“服务端替用户访问了不可信目标”。修复时不能只补一个正则或黑名单,而要围绕协议、域名、DNS、IP、重定向、网络出口和运行环境做完整约束。
从实际经验看,最有效的组合是:统一安全请求组件、关闭自动重定向并逐跳校验、严格限制目标范围、抓取服务网络隔离、日志可观测。这样即使某一层判断有遗漏,也不至于直接扩大成高风险问题。
建议把 SSRF 防护沉到基础组件里,而不是让每个业务接口各写一套校验。安全边界越分散,后续越难维护。
推荐意见