背景SSRF(Server-Side Request Forgery,服务端请求伪造)属于 Web 安全里比较常见但容易被低估的一类问题。它的本质是:应用服务端根据用户输入去发起网络请求,但没有对目标地址、协议、跳转、解析结果做足够限制,导致攻击者可以借服务端身份访问不该访问的资源。在真实项目里,SSRF 往往出现在“看起来很正常”的业务功能中,例如 URL 预览、图片抓取、远程文件导入、Webhook 回调测试、RSS 订阅、PDF/截图生成、对象存储迁移等。代码审计时如果只关注 SQL 注入、XSS、反序列化,容易漏掉这类服务端出网能力带来的风险。本文重点放在防御性代码审计、受控环境复现和修复思路,不涉及未授权攻击或利用真实内网资产。常见问题场景下面这些代码模式,在审计时需要提高敏感度:后端接收 url、link、target、callback、avatar、image、feed 等参数后直接请求。允许用户填写 Webhook 地址,并提供“测试发送”功能。服务端将用户传入的图片地址下载到本地或上传到对象存储。使用浏览器内核或截图服务访问用户提供的页面。后端请求前只做了字符串黑名单过滤,例如禁止包含 127.0.0.1 或 localhost。只校验了首次解析结果,没有处理 DNS 重绑定、302 跳转、IPv6、十进制/八进制 IP 等情况。从风险角度看,SSRF 不一定都能造成严重后果,但它可能成为访问内部管理面、云元数据服务、Redis/Elasticsearch 等内网服务的入口。即便请求结果不回显,也可能通过响应时间、DNS 日志、错误信息形成盲打探测面。代码审计切入点审计 SSRF 时,我一般先找“出网函数”和“用户输入”的交汇点。不同语言生态里关注的 API 略有差异。语言/框架常见关注点JavaHttpURLConnection、OkHttp、Apache HttpClient、RestTemplate、WebClientPHPcurl_exec、file_get_contents、fopen、fsockopen、GuzzlePythonrequests、urllib、httpx、aiohttpGonet/http、http.Client、url.Parse、net.DialNode.jsaxios、node-fetch、request、http/https 模块一个比较典型的危险写法如下:// Java 示例:仅用于说明风险模式
@GetMapping(\"/fetch\")
public String fetch(@RequestParam String url) throws Exception {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(url, String.class);
}这类代码的问题不是“用了 RestTemplate”,而是用户完全控制了请求目标,服务端没有建立任何可信边界。即使业务上只想支持外部 HTTP 图片,也需要明确协议、域名、IP 范围、跳转策略、超时和响应大小。受控环境复现思路在授权测试或本地靶场环境中,可以用一个最小化服务来验证风险链路。这里给一个防御视角的实验配置,不包含真实目标探测。# 启动一个本地 HTTP 服务,用于观察服务端是否会发起请求
python3 -m http.server 8000然后在被测应用的 URL 抓取功能中填入测试地址,例如: HTTP 服务位于同一环境,HTTP 服务日志中出现访问记录,就说明后端确实根据输入发起了请求。接下来应继续验证:是否允许访问内网地址、是否跟随跳转、是否限制协议、是否能读取响应内容、是否存在响应差异。为了避免误伤,复现时建议只使用本地或隔离网络中的服务,例如:127.0.0.1 上的测试 HTTP 服务。Docker Compose 里单独创建的模拟内网服务。自建 DNS 日志服务或测试域名,不指向第三方资产。一个简单的 Docker Compose 隔离环境可以这样组织:services:
app:
image: your-test-app:latest
ports:
- \"8080:8080\"
networks:
- labnet
internal:
image: nginx:alpine
networks:
- labnet
networks:
labnet:
driver: bridge在这种环境里,可以观察应用是否能访问 internal 服务,从而判断是否存在服务端越界访问问题。技术分析要点1. 协议限制不能只靠前缀判断很多代码会写成:if (!url.startsWith(\" && !url.startsWith(\" {
throw new IllegalArgumentException(\"invalid url\");
}这只能挡住明显的非 HTTP 协议,并不能解决目标地址可信问题。正确做法应使用标准 URL 解析器解析 scheme,再判断是否只允许 http/https,同时禁止用户信息段、异常端口等不必要特性。2. 域名白名单优于黑名单黑名单很难覆盖所有内网表示方式,例如 IPv6、本地回环、特殊编码、短地址、十进制 IP、DNS 解析变化等。业务如果明确只允许访问固定对象存储域名、图片 CDN、合作方 API,优先做白名单。白名单建议基于规范化后的 host 判断,不要直接对原始字符串做 contains 判断。例如 example.com.evil.test 不能被当作 example.com。3. DNS 解析后还要校验 IP 范围仅校验域名格式不够,因为域名可以解析到内网地址。比较稳妥的流程是:解析 URL,提取 scheme、host、port。对 host 做 DNS 解析,获取所有 A/AAAA 记录。拒绝私有地址、回环地址、链路本地地址、组播地址、保留地址。实际连接时绑定已经校验过的解析结果,避免校验和连接之间发生解析变化。Go 里可以使用 net 包判断 IP 范围,示例只展示核心思路:func isBlockedIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
ip.IsMulticast() || ip.IsUnspecified() {
return true
}
privateCIDRs := []string{
\"10.0.0.0/8\",
\"172.16.0.0/12\",
\"192.168.0.0/16\",
\"127.0.0.0/8\",
\"169.254.0.0/16\",
\"::1/128\",
\"fc00::/7\",
\"fe80::/10\",
}
for _, cidr := range privateCIDRs {
_, block, _ := net.ParseCIDR(cidr)
if block.Contains(ip) {
return true
}
}
return false
}4. 跳转需要重新校验SSRF 修复里很常见的漏洞是:首次 URL 做了校验,但 HTTP 客户端默认跟随 302/301 跳转,跳转后的地址没有校验。结果攻击者可以提交一个看似合法的外部地址,再由它跳转到受限地址。修复方式有两个方向:禁用自动跳转,业务上确实需要时手动处理 Location 并重新走完整校验。限制最大跳转次数,避免循环跳转和链式跳转。// Go 示例:禁用自动跳转
client := &http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}5. 响应大小和超时同样重要即使 SSRF 目标受限,如果没有设置超时和响应大小限制,也可能被利用造成资源消耗。建议至少设置:连接超时、读取超时、整体请求超时。最大响应体大小,例如图片抓取限制为几 MB。Content-Type 校验,但不要只信响应头,必要时做文件魔数校验。禁止请求非必要端口,例如只允许 80/443 或业务指定端口。// Java 示例:限制读取大小的思路
InputStream in = response.getBody();
byte[] data = in.readNBytes(2 * 1024 * 1024 + 1);
if (data.length > 2 * 1024 * 1024) {
throw new IllegalArgumentException(\"response too large\");
}修复建议清单落地修复时,可以按下面的顺序处理:优先判断该功能是否真的需要让用户输入任意 URL;能改成上传文件或选择固定资源的,不开放远程抓取。建立允许访问的域名白名单,避免黑名单维护。只允许 http/https,禁止 file、gopher、ftp、dict 等协议。解析并规范化 URL,禁止用户信息段,限制端口。DNS 解析后校验所有返回 IP,拒绝内网、回环、链路本地、保留地址。禁用默认跳转,或对每一次跳转后的 URL 重新校验。设置合理超时、最大响应体、最大重定向次数。对请求结果做类型校验,图片类业务不要只看扩展名或 Content-Type。记录安全日志,包括原始 URL、规范化 host、解析 IP、拒绝原因,但避免记录敏感响应内容。在 CI 中加入 SSRF 相关单元测试,覆盖内网 IP、IPv6、跳转、异常端口等用例。测试用例设计为了避免修复后被边界情况绕过,建议补充一些自动化测试。下面是测试维度示例,具体 payload 应只在本地或授权环境使用。测试类型预期结果普通外部白名单域名允许非白名单域名拒绝127.0.0.1 / localhost拒绝10.0.0.0/8、172.16.0.0/12、192.168.0.0/16拒绝IPv6 回环与 ULA 地址拒绝合法域名跳转到受限地址拒绝或不跟随超大响应体中断并记录慢响应服务超时退出如果是 Java 项目,可以把 URL 校验逻辑封装成独立组件,再用单元测试固定行为,避免每个业务点各写一套不一致的判断。审计中的几个容易忽略点图片代理接口经常被当成“低危”,但它同样具备服务端出网能力。后台管理功能也要审计,SSRF 不一定只出现在前台入口。只限制请求结果不回显不等于安全,盲 SSRF 仍然可能产生风险。容器环境中要关注服务名访问,例如同一 Docker 网络内的服务发现。云环境要特别关注元数据访问风险,建议结合网络层策略禁用不必要的元数据访问。安全网关或 WAF 不能替代业务代码修复,因为很多请求发生在服务端内部。网络层加固代码修复之外,服务端出网策略也很关键。实际项目里,我更倾向于做双层控制:应用层限制 URL,网络层限制 egress。比如:生产环境默认禁止应用容器访问内网管理网段。只允许访问必要的外部 API 或对象存储地址。对云元数据地址做网络层阻断,避免单点代码缺陷扩大影响。为远程抓取类服务单独部署低权限 worker,不和核心业务、数据库处在同一信任域。Kubernetes 环境可以通过 NetworkPolicy 控制出站访问,具体策略要结合 CNI 是否支持 egress enforcement。不要只写策略文件而不验证生效情况。总结SSRF 的难点不在于“发现一个能请求的参数”,而在于准确判断服务端出网边界是否失控,并给出可长期维护的修复方案。单纯依赖字符串黑名单、禁止 localhost、限制 http 前缀,通常都不够。比较稳妥的思路是:业务上尽量减少任意 URL 输入;必须支持时,使用白名单、规范化解析、DNS 后 IP 校验、跳转重校验、超时和大小限制;同时用网络层 egress 策略兜底。这样即便某个业务点出现疏漏,也不至于让服务端成为访问内部资源的跳板。
推荐意见