跳转到帖子

背景最近整理一批内网 Web 资产的代码审计记录,发现 SSRF 仍然是比较容易被低估的问题。原因不复杂:很多业务都需要“服务端代请求”,比如图片抓取、Webhook 回调、URL 预览、文件转存、在线文档导入等。开发侧往往只关注功能是否可用,安全侧如果只看入口参数,也容易漏掉后续重定向、DNS 解析、协议支持等细节。这篇文章不讨论攻击利用,也不提供对真实目标的操作方法,只结合代码审计和本地复现环境,梳理 SSRF 风险的常见触发点、审计思路和加固方式。重点放在长期可复用的方法上,适合做内部安全基线或代码审计 checklist。典型场景在 Web 业务中,SSRF 常见于以下功能:用户提交 URL,服务端抓取图片、头像、封面或网页标题。Webhook、回调地址、通知地址由用户配置。远程文件导入,例如从 URL 下载文档、压缩包、配置文件。富文本编辑器自动解析外部资源。微服务网关或代理类接口,由后端根据参数拼接请求。这些功能的共同点是:服务端会根据用户可控输入发起网络请求。一旦缺少严格限制,服务端就可能被迫访问不该访问的地址,例如内网服务、云平台元数据接口、管理后台或本机监听端口。问题代码示例下面是一段简化后的 Java 示例,逻辑看起来很正常:校验 URL 是否以 http 开头,然后下载内容。public byte[] fetch(String inputUrl) throws IOException {
if (!inputUrl.startsWith(\"http\")) {
throw new IllegalArgumentException(\"invalid url\");
}

URL url = new URL(inputUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);

try (InputStream in = conn.getInputStream()) {
return in.readAllBytes();
}
}这类代码的问题主要有几个:只校验字符串前缀,没有限制协议范围。没有解析 hostname 后判断 IP 归属。没有限制重定向后的目标地址。没有限制响应大小,容易造成资源消耗。没有限制 Content-Type,业务上可能并不需要任意内容。很多 SSRF 问题并不是因为完全没有校验,而是校验点过于靠前,且没有覆盖请求过程中的变化。审计思路代码审计时,我一般会从“入口、解析、请求、重定向、响应处理”这几层拆开看。1. 查找服务端请求点不同语言的关键函数不同,可以先通过关键词粗筛。比如 Java 项目中重点关注:java.net.URLHttpURLConnectionApache HttpClientOkHttpClientRestTemplateWebClient可以用 ripgrep 快速搜索:rg -n \"URL\\(|HttpURLConnection|OkHttpClient|RestTemplate|WebClient|HttpClient\" src/Python 项目中可关注:rg -n \"requests\\.|urllib|aiohttp|httpx|urlopen\" .Node.js 项目中可关注:rg -n \"axios|request\\(|fetch\\(|http\\.request|https\\.request|got\\(\" .搜索到调用点后,不要只看这一行,重点追踪参数来源。只要请求目标包含用户可控部分,就需要进一步分析。2. 判断输入是否真正可控需要确认 URL 的来源:是否来自 HTTP 参数、JSON body、Header、Cookie。是否来自数据库中用户可配置字段。是否来自消息队列、Webhook 配置、第三方同步数据。是否经过后端拼接,用户只控制 path、host 或 query 的一部分。有些问题不是用户直接提交完整 URL,而是只控制 host:String api = \" + tenantDomain + \"/api/profile\";这种拼接方式同样需要做域名白名单或租户域名绑定校验,不能只假设 tenantDomain 一定可信。3. 检查协议限制很多库支持的协议不止 HTTP/HTTPS。即使当前代码看似只做 HTTP 请求,也建议显式限制协议:URI uri = URI.create(inputUrl);
String scheme = uri.getScheme();

if (!\"http\".equalsIgnoreCase(scheme) && !\"https\".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException(\"unsupported scheme\");
}注意不要用 startsWith(\"http\") 这类字符串判断。URL 解析中的大小写、空白字符、特殊编码、userinfo 等细节都可能让简单字符串校验失效。4. 检查 DNS 解析和 IP 归属只做域名黑名单通常不够。更稳妥的方式是解析 hostname 后判断 IP 是否落在禁止访问的网段。通常需要禁止:127.0.0.0/8,本机地址。10.0.0.0/8,私有地址。172.16.0.0/12,私有地址。192.168.0.0/16,私有地址。169.254.0.0/16,链路本地地址。::1/128,本机 IPv6。fc00::/7,IPv6 唯一本地地址。fe80::/10,IPv6 链路本地地址。Java 中可以先用 InetAddress 做基础判断,再结合自定义网段库补充。示例仅表达思路:InetAddress address = InetAddress.getByName(uri.getHost());

if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()) {
throw new IllegalArgumentException(\"private address is not allowed\");
}实际生产中还要注意 IPv4-mapped IPv6、十进制 IP、八进制 IP、短写 IP、域名多 A 记录等情况。安全网关或统一请求组件里建议使用成熟的 IP/CIDR 判断库,而不是手写字符串匹配。5. 处理 DNS Rebinding 风险一个容易被忽略的问题是 DNS Rebinding。代码先解析域名并判断为公网 IP,但真正建立连接时,HTTP 客户端可能再次解析,得到另一个地址。加固时可以考虑:在统一 HTTP 客户端层自定义 DNS 解析器,复用校验后的解析结果。连接建立后获取实际远端地址,再做一次校验。对外部 URL 抓取功能使用独立网络出口,默认无法访问内网网段。对域名白名单业务,优先做严格白名单,而不是黑名单。单靠应用层校验很难覆盖全部网络边界问题,最好配合 egress 策略一起做。6. 检查重定向重定向是 SSRF 审计中的高频漏点。初始 URL 合法,不代表 302 后的 Location 仍然合法。建议关闭自动重定向,手动处理每一次跳转,并对跳转后的 URL 重新执行完整校验。HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(false);

int status = conn.getResponseCode();
if (status >= 300 && status < 400) {
String location = conn.getHeaderField(\"Location\");
// 解析相对跳转地址,再重新走 URL 校验逻辑
}跳转次数也要限制,避免循环跳转导致资源消耗。一般业务场景设置 3 到 5 次已经足够。7. 限制端口和响应大小如果业务只是抓取图片或网页标题,没有必要允许访问任意端口。建议只允许 80 和 443,确有需要再按业务白名单开放。int port = uri.getPort();
if (port != -1 && port != 80 && port != 443) {
throw new IllegalArgumentException(\"port is not allowed\");
}响应大小也要限制。不要直接 readAllBytes 读取任意长度内容,建议使用带上限的读取方式。例如最大只读取 2MB:private byte[] readWithLimit(InputStream in, int maxBytes) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int total = 0;
int n;

while ((n = in.read(buffer)) != -1) {
total += n;
if (total > maxBytes) {
throw new IOException(\"response too large\");
}
out.write(buffer, 0, n);
}
return out.toByteArray();
}8. 校验 Content-Type很多业务只需要图片,却允许下载任意二进制内容。更稳妥的做法是同时检查响应头和实际文件魔数。响应头可作为第一层过滤:String contentType = conn.getContentType();
if (contentType == null || !contentType.toLowerCase().startsWith(\"image/\")) {
throw new IllegalArgumentException(\"invalid content type\");
}但不要完全相信 Content-Type。后续保存、解析、展示时,还要结合文件头校验和安全的存储策略,避免引入文件上传类风险。推荐的统一封装方式如果项目里有多处远程 URL 抓取功能,建议不要让各业务模块自行 new HTTP client。比较可控的方式是封装一个 SafeHttpFetcher,统一做以下事情:只允许 http 和 https。禁止私有地址、本机地址、链路本地地址。默认只允许 80/443 端口。关闭自动重定向,手动校验每次跳转。限制连接超时、读取超时和总耗时。限制响应大小。按业务限制 Content-Type。记录安全审计日志,但避免记录敏感响应内容。统一封装的价值在于:后续发现边界 case 时,只需要改一处。代码审计时也能快速判断是否走了安全组件。网络层加固应用层校验不是 SSRF 防护的全部。实际项目中,更建议做网络层兜底:抓取外部 URL 的服务单独部署,网络权限最小化。禁止该服务访问内网管理网段、数据库网段、元数据地址。Kubernetes 环境中使用 NetworkPolicy 限制 egress。云环境中关闭不必要的实例元数据访问,或启用更严格的元数据访问版本和 hop limit。通过出口代理统一访问互联网,并在代理层做域名和 IP 策略。很多严重影响并不是 SSRF 本身造成的,而是服务端所在网络位置权限过大。把网络边界收紧,能显著降低单点漏洞的影响面。审计 Checklist检查项关注点入口来源URL 是否来自用户输入、配置项、数据库或第三方消息协议限制是否只允许 http/https,是否使用标准 URI 解析域名/IP 校验是否解析后判断私有地址、本机地址、链路本地地址重定向是否对每次跳转后的地址重新校验端口限制是否默认只允许业务需要的端口超时控制是否设置连接超时、读取超时、总耗时响应限制是否限制响应大小和 Content-Type网络隔离服务是否具备访问敏感内网资源的能力日志审计是否记录异常请求,是否避免记录敏感响应内容容易踩的坑只用黑名单拦截 127.0.0.1、localhost,忽略 IPv6 和其他私有网段。只校验第一次请求,忽略 301/302/307/308 跳转。校验域名字符串,不校验最终解析出的 IP。在业务代码里散落多个 HTTP 请求实现,导致修复不一致。允许任意端口,给内部服务探测留下空间。没有响应大小限制,可能引发内存或磁盘资源消耗。以为内网系统“只有自己能访问”,忽略服务端请求能力。总结SSRF 的防护关键不在某一个 if 判断,而在请求生命周期的完整控制:输入解析、协议限制、DNS 解析、IP 判断、端口限制、重定向处理、响应限制和网络隔离都要覆盖到。代码审计时也建议按这个链路逐层拆解,不要只看入口过滤。从长期维护角度看,最推荐的方案是“统一安全请求组件 + 网络层 egress 限制”。前者降低业务代码出错概率,后者限制漏洞影响范围。两者结合,才能把这类问题从单点修补变成可持续治理。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…