跳转到帖子

关于这间俱乐部

应急响应中心
  1. 这间俱乐部的新消息
  2. 背景SSRF(Server-Side Request Forgery)在企业内部系统里并不少见,尤其是带有“URL 预览、远程图片抓取、Webhook 回调、文件导入、第三方接口代理”等能力的业务。它的问题不在于某一行代码有多复杂,而是服务端代替用户发起了网络请求,如果边界没收紧,就可能访问到本不该暴露的内部资源。 这篇文章整理一次比较典型的 Java Web 代码审计过程,重点放在识别风险点、分析常见绕过、设计可落地的修复方案。内容偏防守视角,不涉及对真实目标的攻击操作。 场景描述业务里有一个“远程资源导入”功能,用户提交一个 URL,后端拉取资源并保存到对象存储。简化后的代码如下: @PostMapping("/import") public ResponseEntity<String> importResource(@RequestParam String url) throws Exception { RestTemplate restTemplate = new RestTemplate(); byte[] data = restTemplate.getForObject(url, byte[].class); String objectKey = storageService.save(data); return ResponseEntity.ok(objectKey); }从功能上看很直接,但从安全角度看,风险也很明显:用户可控的 URL 被服务端直接请求,没有协议限制、没有目标地址校验、没有重定向控制、没有响应大小限制,也没有请求超时策略。 风险点分析1. 协议未限制很多代码只判断字符串是否以 http 开头,甚至完全不判断协议。这样容易引入非预期协议访问风险。即使当前 HTTP 客户端默认只支持 HTTP/HTTPS,也不建议把安全假设建立在库的默认行为上。 建议明确只允许: httphttps其他协议直接拒绝。 2. 域名与 IP 校验缺失SSRF 的核心问题通常是访问内部地址,例如内网网段、本机地址、链路本地地址等。审计时需要重点检查是否存在以下情况: 允许访问 127.0.0.1、localhost允许访问 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16允许访问 169.254.0.0/16 这类链路本地地址只校验域名字符串,不校验解析后的 IP只校验第一次请求,不校验重定向后的目标仅靠黑名单字符串拦截通常不可靠,因为域名解析、IPv6、十进制/八进制 IP 表示、重定向等都会让判断变复杂。更稳妥的方式是做“允许列表”或解析后的地址校验。 3. 重定向未重新校验不少 HTTP 客户端会自动跟随 30x 重定向。如果业务只校验初始 URL,没有校验跳转后的 URL,那么校验就可能失效。 更安全的做法是关闭自动重定向,手动处理每一次跳转,并在每次跳转前重新执行完整校验,同时限制跳转次数。 4. 响应大小与超时缺失即使没有访问内部资源,攻击者也可能提交一个响应极慢或响应体极大的地址,导致线程阻塞、内存占用升高,最终影响服务稳定性。因此 SSRF 防护不只关心“访问哪里”,也要关心“请求多久、下载多少”。 建议至少设置: 连接超时读取超时最大响应体大小最大重定向次数允许的 Content-Type 范围审计思路做 Java Web 代码审计时,可以从“用户输入到网络请求”的数据流入手。常见危险调用包括: RestTemplate#getForObjectRestTemplate#exchangeHttpURLConnection#connectOkHttpClient#newCallApache HttpClient#executeURL#openStreamImageIO#read(URL)可以先用代码搜索定位关键调用: grep -R "RestTemplate\|openStream\|HttpURLConnection\|OkHttpClient\|ImageIO.read" -n src/main/java定位后再向上追踪参数来源,判断 URL 是否来自用户输入、配置中心、消息队列、数据库字段或第三方回调。很多风险并不直接出现在 Controller 层,而是在异步任务、导入任务、图片处理服务中。 关键修复方案1. 使用 URI 解析而不是字符串拼接判断不要用简单的 startsWith 或正则处理完整 URL。应使用标准 URI 解析,提取 scheme、host、port 等字段后再判断。 URI uri = URI.create(inputUrl); String scheme = uri.getScheme(); String host = uri.getHost(); if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { throw new IllegalArgumentException("unsupported scheme"); } if (host == null || host.isBlank()) { throw new IllegalArgumentException("invalid host"); }2. 解析域名并校验所有 IP如果允许用户提交域名,应解析该域名对应的所有 IP,并逐一判断是否落在禁止访问的地址范围内。 private boolean isPrivateOrLocalAddress(InetAddress address) { return address.isAnyLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress() || address.isMulticastAddress(); } private void validateHost(String host) throws UnknownHostException { InetAddress[] addresses = InetAddress.getAllByName(host); for (InetAddress address : addresses) { if (isPrivateOrLocalAddress(address)) { throw new IllegalArgumentException("target address is not allowed"); } } }这里的示例主要覆盖常见本地、内网、链路本地地址。实际生产环境还应根据网络架构补充公司内部网段、云厂商元数据地址、办公网段等。 3. 优先采用业务允许列表如果业务场景明确,例如只允许导入几个合作方的资源,最好直接使用域名允许列表。相比维护复杂黑名单,允许列表更容易解释和验证。 private static final Set<String> ALLOWED_HOSTS = Set.of( "static.example.com", "assets.partner.example" ); private void validateAllowedHost(String host) { String normalized = host.toLowerCase(Locale.ROOT); if (!ALLOWED_HOSTS.contains(normalized)) { throw new IllegalArgumentException("host is not allowed"); } }如果需要支持子域名,也建议明确后缀匹配规则,避免 example.com.evil.test 这类误判。可以采用“等于主域名或以点加主域名结尾”的方式: private boolean matchDomain(String host, String domain) { host = host.toLowerCase(Locale.ROOT); domain = domain.toLowerCase(Locale.ROOT); return host.equals(domain) || host.endsWith("." + domain); }4. 禁止自动重定向,逐跳校验使用 Apache HttpClient 时,可以关闭自动重定向,再根据响应头中的 Location 手动处理。每一次跳转都要重新解析 URL、校验协议、校验 Host、解析 IP,并限制跳转次数。 RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) .setResponseTimeout(5000) .setRedirectsEnabled(false) .build(); CloseableHttpClient client = HttpClients.custom() .setDefaultRequestConfig(config) .disableRedirectHandling() .build();重定向处理建议控制在 3 次以内。超过次数直接失败,不要为了兼容性无限跳转。 5. 限制响应体大小不要直接把响应一次性读入内存。可以边读边统计大小,超过阈值立即中断。 private byte[] readWithLimit(InputStream input, int maxBytes) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int total = 0; int n; while ((n = input.read(buffer)) != -1) { total += n; if (total > maxBytes) { throw new IOException("response too large"); } output.write(buffer, 0, n); } return output.toByteArray(); }具体阈值要结合业务,例如头像类资源可以限制到几 MB,普通文档导入可以再放宽,但不建议无限制。 一套相对完整的校验流程可以把服务端请求外部 URL 的流程整理成固定模板,避免不同业务各写一套半成品校验: 解析 URI,拒绝非法 URL只允许 HTTP/HTTPS校验 Host 是否在允许范围内解析 Host 对应的全部 IP拒绝本地地址、内网地址、链路本地地址和公司内部敏感网段设置连接超时和读取超时关闭自动重定向,逐跳重新校验限制最大响应大小限制允许的响应类型记录审计日志,便于排查异常请求测试用例建议修复后建议补充单元测试和集成测试。测试重点不是“能不能请求成功”,而是边界是否被正确拒绝。 测试项预期结果HTTP/HTTPS 正常公网域名允许,前提是符合业务允许范围file、ftp 等非 HTTP 协议拒绝localhost、本机回环地址拒绝内网 IPv4 地址拒绝IPv6 本地地址拒绝公网地址重定向到内网地址拒绝响应体超过限制中断并返回失败连接超时或读取超时失败且不阻塞主流程如果团队内有统一安全测试环境,可以搭一个只用于验证的本地 HTTP 服务,模拟重定向、超大响应、慢响应等情况。注意测试目标应限定在本地或授权环境内。 日志与监控SSRF 类问题修复后,也建议保留必要的审计日志。日志不要记录完整敏感参数,但应能支持排查: 调用方用户或业务标识原始 Host解析后的 IP 列表最终请求结果失败原因耗时与响应大小如果短时间内出现大量被拒绝的内网地址、异常协议、重定向失败,可以作为安全告警信号。 常见误区只用正则判断 URL 格式,未解析真实 Host。只拦截 127.0.0.1,忽略 IPv6、本机名、内网网段。只校验第一次请求,忽略重定向。认为业务在内网就安全,实际上服务端能访问的资源往往更多。直接复用通用 HTTP 客户端,没有设置超时和大小限制。把 SSRF 防护写在某个 Controller,其他异步任务继续裸请求。总结SSRF 的修复重点不是单个判断条件,而是建立一条完整的服务端出站请求安全链路。对代码审计来说,重点关注用户可控 URL 是否进入网络请求 API;对修复来说,优先考虑允许列表、解析后 IP 校验、重定向逐跳校验、超时和响应大小限制。 实际项目中,建议把这套逻辑沉淀成统一组件,禁止业务代码直接使用裸 HTTP 客户端访问用户输入的 URL。这样既能减少重复实现,也能避免后续新增功能时再次引入同类问题。 代码审计、调用链与关键函数定位示意 背景 在企 企业 业内 内部 部系 系统 统里 里并 并不 不少 少见 在企业 企业内 业内部 内部系 部系统 系统里 统里并 里并不 并不少 不少见 在企业内 企业内部 业内部系 内部系统 部系统里 系统里并 统里并不 里并不少 并不少见 尤其 其是 是带 带有 尤其是 其是带 是带有 尤其是带 其是带有 预览 远程 程图 图片 片抓 抓取 远程图 程图片 图片抓 片抓取 远程图片 程图片抓 图片抓取 回调 文件 件导 导入 文件导 件导入 文件导入 第三 三方 方接 接口 口代 代理 第三方 三方接 方接口 接口代 口代理 第三方接 三方接口 方接口代 接口代理 等能 能力 力的 的业 业务 等能力 能力的 力的业 的业务 等能力的 能力的业 力的业务 它的 的问 问题 题不 不在 在于 于某 某一 一行 行代 代码 码有 有多 多复 复杂 它的问 的问题 问题不 题不在 不在于 在于某 于某一 某一行 一行代 行代码 代码有 码有多 有多复 多复杂 它的问题 的问题不 问题不在 题不在于 不在于某 在于某一 于某一行 某一行代 一行代码 行代码有 代码有多 码有多复 有多复杂 而是 是服 服务 务端 端代 代替 替用 用户 户发 发起 起了 了网 网络 络请 请求 而是服 是服务 服务端 务端代 端代替 代替用 替用户 用户发 户发起 发起了 起了网 了网络 网络请 络请求 而是服务 是服务端 服务端代 务端代替 端代替用 代替用户 替用户发 用户发起 户发起了 发起了网 起了网络 了网络请 网络请求 如果 果边 边界 界没 没收 收紧 如果边 果边界 边界没 界没收 没收紧 如果边界 果边界没 边界没收 界没收紧 就可 可能 能访 访问 问到 到本 本不 不该 该暴 暴露 露的 的内 部资 资源 就可能 可能访 能访问 访问到 问到本 到本不 本不该 不该暴 该暴露 暴露的 露的内 的内部 内部资 部资源 就可能访 可能访问 能访问到 访问到本 问到本不 到本不该 本不该暴 不该暴露 该暴露的 暴露的内 露的内部 的内部资 内部资源 这篇 篇文 文章 章整 整理 理一 一次 次比 比较 较典 典型 型的 这篇文 篇文章 文章整 章整理 整理一 理一次 一次比 次比较 比较典 较典型 典型的 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次比 一次比较 次比较典 比较典型 较典型的 码审 审计 计过 过程 代码审 码审计 审计过 计过程 代码审计 码审计过 审计过程 重点 点放 放在 在识 识别 别风 风险 险点 重点放 点放在 放在识 在识别 识别风 别风险 风险点 重点放在 点放在识 放在识别 在识别风 识别风险 别风险点 分析 析常 常见 见绕 绕过 分析常 析常见 常见绕 见绕过 分析常见 析常见绕 常见绕过 设计 计可 可落 落地 地的 的修 修复 复方 方案 设计可 计可落 可落地 落地的 地的修 的修复 修复方 复方案 设计可落 计可落地 可落地的 落地的修 地的修复 的修复方 修复方案 内容 容偏 偏防 防守 守视 视角 内容偏 容偏防 偏防守 防守视 守视角 内容偏防 容偏防守 偏防守视 防守视角 不涉 涉及 及对 对真 真实 实目 目标 标的 的攻 攻击 击操 操作 不涉及 涉及对 及对真 对真实 真实目 实目标 目标的 标的攻 的攻击 攻击操 击操作 不涉及对 涉及对真 及对真实 对真实目 真实目标 实目标的 目标的攻 标的攻击 的攻击操 攻击操作 场景 景描 描述 述业 务里 里有 有一 一个 场景描 景描述 描述业 述业务 业务里 务里有 里有一 有一个 场景描述 景描述业 描述业务 述业务里 业务里有 务里有一 里有一个 程资 源导 远程资 程资源 资源导 源导入 远程资源 程资源导 资源导入 功能 户提 提交 交一 用户提 户提交 提交一 交一个 用户提交 户提交一 提交一个 后端 端拉 拉取 取资 源并 并保 保存 存到 到对 对象 象存 存储 后端拉 端拉取 拉取资 取资源 资源并 源并保 并保存 保存到 存到对 到对象 对象存 象存储 后端拉取 端拉取资 拉取资源 取资源并 资源并保 源并保存 并保存到 保存到对 存到对象 到对象存 对象存储 简化 化后 后的 的代 码如 如下 简化后 化后的 后的代 的代码 代码如 码如下 简化后的 化后的代 后的代码 的代码如 代码如下 从功 能上 上看 看很 很直 直接 从功能 功能上 能上看 上看很 看很直 很直接 从功能上 功能上看 能上看很 上看很直 看很直接 但从 从安 安全 全角 角度 度看 但从安 从安全 安全角 全角度 角度看 但从安全
  3. 在实际的 Web 安全事件中,最难处理的往往不是发现某个可疑文件,而是回答三个问题:攻击者从哪里进入、哪些数据可能被读取或篡改、修复后是否还存在残留访问路径。只删除一个所谓的“木马文件”,通常无法完成处置,甚至可能破坏后续取证。 本文以一次典型的 WebShell 告警为背景,整理从告警确认、证据保全、日志关联到代码审计和修复验证的完整思路。示例以常见的 PHP 应用为主,但其中的方法同样适用于 Java、Node.js 和 Python Web 服务。 一、事件背景与初步判断假设主机安全产品报告 Web 目录下出现可疑 PHP 文件,文件内容包含动态执行函数,且该文件在凌晨被访问过。此时不应立即删除文件或重启主机,而应先确认告警是否具备真实攻击特征。 初步判断可以围绕以下几个维度展开: 文件的创建时间、修改时间和属主是否异常,是否与发布窗口一致。访问日志中是否存在对该文件的外部请求,以及请求来源、User-Agent 和响应状态。文件是否被其他脚本包含,或者是否存在同目录下的关联文件。应用进程、计划任务、启动项和 Web 服务配置是否出现异常变更。数据库、对象存储和业务日志中是否出现异常读取或批量修改。其中,单独依赖文件名或关键字匹配容易产生误判。某些业务代码确实会使用反射、模板执行或动态加载机制;相反,攻击者也可能使用普通文件名、拆分字符串或编码内容来规避简单扫描。 二、先保全证据,再进行清理事件响应的第一原则是保留现场。对于疑似入侵主机,建议先限制其对外访问范围,保留管理通道,并在不改变原始证据的前提下进行采集。若业务无法停机,可以先将流量切换到干净实例,再对原实例做离线分析。 采集时至少应保存以下内容: Web 目录、上传目录、临时目录和应用发布目录的文件清单。Web 访问日志、错误日志、反向代理日志和 WAF 告警记录。应用部署记录、版本控制提交记录以及 CI/CD 执行记录。系统登录日志、进程列表、监听端口、计划任务和服务配置。数据库审计日志、对象存储访问日志以及密钥使用记录。在 Linux 环境下,可以先生成文件清单和哈希,后续再对比文件变化。下面的命令只用于证据采集,不会修改目标文件: mkdir -p /secure/evidence find /var/www/app -type f -printf '%T@ %s %p\n' | sort -n > /secure/evidence/files.txt find /var/www/app -type f -print0 | xargs -0 sha256sum > /secure/evidence/sha256.txt ps auxww > /secure/evidence/processes.txt ss -lntup > /secure/evidence/listeners.txt crontab -l > /secure/evidence/cron-current.txt 2>/dev/null || true实际操作中应记录采集时间、主机标识、操作者和命令输出的校验值。涉及内存、容器层或云主机快照时,还需要结合平台能力保留快照和审计事件,避免只分析文件系统而遗漏驻留进程。 三、通过日志还原攻击链单条访问日志很难说明问题,应把 WAF、反向代理、Web 服务器和应用日志放到同一时间线中。重点关注以下模式: 短时间内对多个不存在路径进行探测,随后访问上传、管理或调试接口。同一来源先提交异常参数,之后访问新出现的脚本文件。请求方法、Content-Type、响应状态和响应体大小出现明显变化。访问时间与文件创建时间高度接近,且请求来源并非正常办公网络或发布系统。出现大量 500 错误、异常堆栈、路径泄露或模板渲染错误。建议将日志统一转换为时间戳,并以请求 ID、客户端地址、反向代理追踪头和用户会话标识进行关联。需要特别注意代理链中的真实来源地址:不能直接相信任意客户端提交的 X-Forwarded-For,只有在明确配置的可信代理范围内才应使用该字段。 如果访问日志中包含敏感参数,不应直接在工单或聊天窗口复制传播。分析时可以对令牌、Cookie、身份证号等字段做脱敏,并限制证据文件的访问权限。 四、代码审计:从文件落地位置反推入口在很多事件中,可疑文件只是最终结果,真正的漏洞位于文件上传、解压、编辑器接口、插件安装或调试功能。审计应从文件写入点开始,而不是只围绕可疑文件做字符串搜索。 重点搜索以下类型的代码路径: 将用户输入拼接到文件路径、模板路径或命令参数中的位置。使用原始文件名、扩展名或 MIME 类型决定保存方式的逻辑。从请求参数读取内容后直接写入磁盘的接口。对压缩包进行解压但没有校验目录穿越的功能。把上传目录放在可被 Web 服务器解释执行的目录下。仅依赖前端校验、黑名单扩展名或客户端提交的 Content-Type。下面是一个常见但不安全的上传逻辑示例。问题不在于是否使用了白名单,而在于最终文件仍然保存于可执行目录,且文件名和内容校验都不充分: $name = $_FILES['file']['name']; $target = __DIR__ . '/uploads/' . $name; move_uploaded_file($_FILES['file']['tmp_name'], $target);更稳妥的设计应同时满足“存储位置不可执行、服务端重新生成文件名、校验真实文件类型、限制大小和权限、记录审计信息”等条件。示例代码如下,具体 MIME 检测和图片解码策略仍应结合业务类型完善: $tmp = $_FILES['file']['tmp_name']; $size = (int) $_FILES['file']['size']; if ($size <= 0 || $size > 5 * 1024 * 1024) { throw new RuntimeException('invalid size'); } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($tmp); $allowed = array('image/jpeg', 'image/png', 'application/pdf'); if (!in_array($mime, $allowed, true)) { throw new RuntimeException('invalid type'); } $storage = '/srv/app-data/uploads'; $id = bin2hex(random_bytes(16)); $target = $storage . '/' . $id; if (!move_uploaded_file($tmp, $target)) { throw new RuntimeException('save failed'); } chmod($target, 0640);对于图片文件,不能只检查 MIME。应尝试使用图像库重新解码并重新编码,去除不需要的元数据;对于 PDF 等复杂格式,应在隔离环境中处理,并通过内容安全策略限制在线预览。上传目录最好位于 Web 根目录之外,若业务必须直接访问,应通过下载接口读取,并设置正确的 Content-Type、Content-Disposition 和权限控制。 五、如何判断是否存在持久化或横向影响确认 Web 目录中的异常文件后,还要排查攻击者是否建立了其他访问路径。检查范围不应只限于当前应用目录,还应覆盖: 系统级和用户级计划任务、systemd 服务、容器启动参数。Web 服务器配置、反向代理规则、应用启动脚本和环境变量。新增用户、SSH 授权密钥、异常 sudo 配置和临时凭据。应用依赖包、插件、构建产物和镜像基础层是否被污染。数据库中的管理员账户、API 密钥、回调地址和定时任务。同一凭据访问过的其他主机、对象存储和代码仓库。这里要区分“观察到的事实”和“推测的影响”。例如,发现某个脚本读取了数据库连接配置,只能说明存在读取可能;是否真的读取了业务数据,还需要结合数据库审计、应用日志、网络流量或访问结果进行确认。事件报告中应明确标注证据等级,避免把推测写成结论。 六、清理、修复与验证清理不应止于删除单个文件。更可靠的方式是从可信代码仓库重新构建应用,并在全新实例上部署。对于无法立即重建的环境,至少应执行以下步骤: 隔离受影响实例,撤销或轮换数据库密码、云访问密钥、应用密钥和管理员会话。移除确认的恶意文件及其关联持久化项,并保存删除前的证据副本。修复导致文件落地或未授权访问的代码路径,补充服务端校验和权限控制。检查 Web 进程的最小权限,禁止其写入代码目录和配置目录。将上传、缓存、日志和临时文件分离,并为敏感目录设置不可执行策略。在测试环境回归验证,再通过灰度发布上线。修复后的验证应包括正向和反向测试:合法文件能够正常上传和访问;伪造扩展名、错误 MIME、超大文件、路径特殊字符、压缩包目录穿越和重复提交等情况都应被拒绝。还要验证异常请求不会把详细堆栈、绝对路径或敏感配置返回给客户端。 七、容易忽略的几个问题只删文件不查入口:如果上传或写文件漏洞仍然存在,清理很快会失效。只看 Web 日志:攻击者可能通过应用任务、消息队列、对象存储或管理接口触发行为。过度依赖杀毒软件:特征库只能帮助发现已知样本,不能替代权限、日志和代码审计。忽略凭据轮换:即使没有证据证明密钥被读取,只要主机曾被完全控制,就应按泄露处理。没有建立基线:缺少发布记录和文件哈希时,很难区分正常变更与恶意变更。直接在线分析样本:处理可疑文件时应使用隔离环境,关闭不必要的网络访问,避免触发外联或二次污染。八、建议建立的长期防护机制一次事件复盘的价值,不只是修补一个漏洞,而是把经验固化到开发、部署和监控流程中。比较实用的改进包括: 在 CI 阶段执行依赖漏洞扫描、静态代码分析和敏感配置检测。对高风险文件写入、权限变更和管理接口建立审计日志。为 Web 目录、上传目录和发布目录建立文件完整性基线。使用不可变部署或只读容器文件系统,减少运行时写入面。对后台接口启用多因素认证、来源限制和细粒度权限控制。将 WAF 告警与主机文件变更、应用发布记录和身份日志关联起来。定期开展备份恢复演练,确认备份未被同一凭据和权限链路污染。总结WebShell 或异常脚本告警只是一个入口,真正的分析目标是还原完整的攻击路径和影响范围。稳妥的流程应当是:先保全证据,再关联日志;从异常文件反推写入入口;检查持久化、凭据和横向影响;最后通过可信重建、凭据轮换和回归测试完成修复。 在技术细节之外,事件响应还需要保持证据意识和边界意识。能被日志证明的事实、基于代码推导出的可能性、以及尚未确认的风险,应分别记录。这样既能提高处置质量,也能让后续开发和运维团队明确知道应该修复什么、验证什么,以及如何避免同类问题再次发生。 代码审计、调用链与关键函数定位示意 在实 实际 际的 在实际 实际的 在实际的 安全 全事 事件 件中 安全事 全事件 事件中 安全事件 全事件中 最难 难处 处理 理的 的往 往往 往不 不是 是发 发现 现某 某个 个可 可疑 疑文 文件 最难处 难处理 处理的 理的往 的往往 往往不 往不是 不是发 是发现 发现某 现某个 某个可 个可疑 可疑文 疑文件 最难处理 难处理的 处理的往 理的往往 的往往不 往往不是 往不是发 不是发现 是发现某 发现某个 现某个可 某个可疑 个可疑文 可疑文件 而是 是回 回答 答三 三个 个问 问题 而是回 是回答 回答三 答三个 三个问 个问题 而是回答 是回答三 回答三个 答三个问 三个问题 攻击 击者 者从 从哪 哪里 里进 进入 攻击者 击者从 者从哪 从哪里 哪里进 里进入 攻击者从 击者从哪 者从哪里 从哪里进 哪里进入 哪些 些数 数据 据可 可能 能被 被读 读取 取或 或篡 篡改 哪些数 些数据 数据可 据可能 可能被 能被读 被读取 读取或 取或篡 或篡改 哪些数据 些数据可 数据可能 据可能被 可能被读 能被读取 被读取或 读取或篡 取或篡改 修复 复后 后是 是否 否还 还存 存在 在残 残留 留访 访问 问路 路径 修复后 复后是 后是否 是否还 否还存 还存在 存在残 在残留 残留访 留访问 访问路 问路径 修复后是 复后是否 后是否还 是否还存 否还存在 还存在残 存在残留 在残留访 残留访问 留访问路 访问路径 只删 删除 除一 一个 个所 所谓 谓的 只删除 删除一 除一个 一个所 个所谓 所谓的 只删除一 删除一个 除一个所 一个所谓 个所谓的 木马 马文 木马文 马文件 木马文件 通常 常无 无法 法完 完成 成处 处置 通常无 常无法 无法完 法完成 完成处 成处置 通常无法 常无法完 无法完成 法完成处 完成处置 甚至 至可 能破 破坏 坏后 后续 续取 取证 甚至可 至可能 可能破 能破坏 破坏后 坏后续 后续取 续取证 甚至可能 至可能破 可能破坏 能破坏后 破坏后续 坏后续取 后续取证 本文 文以 以一 一次 次典 典型 型的 本文以 文以一 以一次 一次典 次典型 典型的 本文以一 文以一次 以一次典 一次典型 次典型的 告警 警为 为背 背景 告警为 警为背 为背景 告警为背 警为背景 整理 理从 从告 警确 确认 整理从 理从告 从告警 告警确 警确认 整理从告 理从告警 从告警确 告警确认 证据 据保 保全 证据保 据保全 证据保全 日志 志关 关联 联到 到代 代码 码审 审计 计和 和修 复验 验证 证的 的完 完整 整思 思路 日志关 志关联 关联到 联到代 到代码 代码审 码审计 审计和 计和修 和修复 修复验 复验证 验证的 证的完 的完整 完整思 整思路 日志关联 志关联到 关联到代 联到代码 到代码审 代码审计 码审计和 审计和修 计和修复 和修复验 修复验证 复验证的 验证的完 证的完整 的完整思 完整思路 示例 例以 以常 常见 见的 示例以 例以常 以常见 常见的 示例以常 例以常见 以常见的 应用 用为 为主 应用为 用为主 应用为主 但其 其中 中的 的方 方法 法同 同样 样适 适用 用于 但其中 其中的 中的方 的方法 方法同 法同样 同样适 样适用 适用于 但其中的 其中的方 中的方法 的方法同 方法同样 法同样适 同样适用 样适用于 服务 件背 景与 与初 初步 步判 判断 断假 假设 设主 主机 机安 全产 产品 品报 报告 事件背 件背景 背景与 景与初 与初步 初步判 步判断 判断假 断假设 假设主 设主机 主机安 机安全 安全产 全产品 产品报 品报告 事件背景 件背景与 背景与初 景与初步 与初步判 初步判断 步判断假 判断假设 断假设主 假设主机 设主机安 主机安全 机安全产 安全产品 全产品报 产品报告 目录 录下 下出 出现 现可 目录下 录下出 下出现 出现可 现可疑 目录下出 录下出现 下出现可 出现可疑 件内 内容 容包 包含 含动 动态 态执 执行 行函 函数 文件内 件内容 内容包 容包含 包含动 含动态 动态执 态执行 执行函 行函数 文件内容 件内容包 内容包含 容包含动 包含动态 含动态执 动态执行 态执行函 执行函数 且该 该文 件在 在凌 凌晨 晨被 被访 问过 且该文 该文件 文件在 件在凌 在凌晨 凌晨被 晨被访 被访问 访问过 且该文件 该文件在 文件在凌 件在凌晨 在凌晨被 凌晨被访 晨被访问 被访问过 此时 时不 不应 应立 立即 即删 除文 件或 或重 重启 启主 此时不 时不应 不应立 应立即
  4. SSRF(服务端请求伪造)并不是“让服务器替客户端发一个 HTTP 请求”这么简单。真正难处理的地方在于:URL 解析存在差异、DNS 解析结果可能变化、重定向会改变目标,以及应用所在网络通常比普通用户拥有更高的访问权限。很多实际事件并非由单一漏洞触发,而是由“任意 URL 请求能力 + 缺少出口控制 + 内部服务认证薄弱”共同造成。 一、问题场景与风险边界 常见业务功能包括图片抓取、远程文件导入、Webhook 测试、在线网页预览、PDF 转换和第三方接口代理。这些功能本身并不等于漏洞,关键在于服务端是否允许用户完全控制请求目标,以及请求是否能够访问不应暴露的网络区域。 应用服务器访问本机回环地址、内网地址或链路本地地址。 应用所在网络可以访问管理面板、配置中心、监控接口等内部服务。 请求组件默认跟随重定向,导致初始校验通过后又跳转到受限地址。 域名校验和实际连接之间存在时间差,DNS 结果发生变化后仍继续连接。 日志只记录用户输入的 URL,没有记录最终解析地址、重定向链和出口策略。 在授权测试或内部实验环境中,应使用专门的模拟服务验证行为,不要直接探测生产内网、云平台元数据或第三方系统。 二、代码审计时的定位方法 审计时不要只搜索“SSRF”或某个 HTTP 库的函数名,而应沿着数据流寻找“用户可控字符串如何变成网络连接目标”。重点关注以下输入来源: HTTP 参数、JSON 字段、表单字段和导入文件中的 URL。 Webhook、回调地址、图片地址、文件下载地址和代理配置。 经过数据库、消息队列或缓存转发后再次进入请求模块的值。 常见的危险汇点包括 Python 的 requests、urllib、Java 的 URLConnection、HttpClient,Node.js 的 fetch、axios,以及 Go 的 http.Client。审计时还要继续跟踪封装层,很多项目会把实际请求隐藏在“下载器”“预览器”或“远程资源解析器”中。 一个有价值的审计记录至少应包含:输入来源、URL 解析位置、协议限制、主机校验、端口限制、DNS 解析方式、是否允许重定向、连接超时、响应大小限制和出口网络策略。只标记“这里调用了 HTTP 客户端”通常不足以判断实际风险。 三、URL 校验中最容易出现的误区 只判断字符串是否以某个域名开头。解析 URL 时应以标准解析器得到的 hostname 为准,而不是直接进行字符串比较。 只阻断 127.0.0.1。还需要考虑 IPv6 回环地址、IPv4 映射地址、私有地址、链路本地地址、未指定地址和其他保留网段。 只校验域名一次。域名可能解析出多个 A 或 AAAA 记录,不能只检查第一个结果。 允许自动跟随重定向。每一次 Location 跳转都必须重新执行协议、主机、端口和 IP 策略校验。 将 DNS 解析和建立连接分成两个不受控制的步骤。校验通过的解析结果不应在实际连接时被重新解析成另一组地址。 把“域名在白名单中”当成完整安全边界。白名单域名被接管、配置错误或返回重定向时,仍可能成为风险入口。 四、一个用于审计的安全校验示例 下面的代码只演示解析和地址判定,不负责实际发起请求。生产实现还需要结合固定出口代理、连接地址绑定和完整的错误处理。示例的重点是:先规范化解析,再检查所有解析结果,而不是用字符串替换或正则表达式代替 URL 解析。 import ipaddress import socket from urllib.parse import urlsplit ALLOWED_SCHEMES = {'http', 'https'} ALLOWED_PORTS = {80, 443} def validate_target(raw_url): parsed = urlsplit(raw_url) scheme = parsed.scheme.lower() if scheme not in ALLOWED_SCHEMES: raise ValueError('scheme denied') if parsed.username or parsed.password: raise ValueError('userinfo denied') if not parsed.hostname: raise ValueError('host missing') try: port = parsed.port or (443 if scheme == 'https' else 80) except ValueError: raise ValueError('invalid port') if port not in ALLOWED_PORTS: raise ValueError('port denied') host = parsed.hostname.rstrip('.').lower() addresses = set() for item in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM): addresses.add(item[4][0]) if not addresses: raise ValueError('no address') for address in addresses: ip = ipaddress.ip_address(address) if not ip.is_global: raise ValueError('non-public address denied') return scheme, host, port, addresses 这个示例仍有几个工程化要求:生产环境应明确处理域名大小写和尾部点号,统一 IPv4 与 IPv6 的表示;应记录实际解析到的地址;建立连接时要确保使用经过校验的地址,避免再次触发不受控解析;同时还要设置连接超时、读取超时、响应大小上限和总耗时上限。 五、推荐的防护架构 单靠业务代码中的黑名单往往难以长期维护,更稳妥的做法是将应用层校验与网络出口控制结合起来。 协议和端口最小化:业务不需要的协议全部禁用,端口使用明确的允许列表,不因“兼容性”开放任意端口。 目标白名单:对于图片、回调等固定场景,优先使用域名或服务标识白名单,而不是允许任意公网地址。 统一出口代理:让业务容器不能直接访问网络,由代理集中执行域名、IP、端口、证书和流量大小策略。 网络分段:应用运行区与管理区、数据库区、控制面隔离,拒绝不必要的东西向访问。 重定向重新校验:建议默认关闭自动重定向;确有业务需求时限制跳转次数,并对每一跳重新校验。 响应最小化:不向用户返回内部服务的完整响应,避免把状态码、响应头和错误堆栈原样暴露。 可观测性:记录请求来源、规范化后的目标、最终连接地址、跳转次数、拒绝原因和响应大小,日志中注意脱敏凭据及敏感查询参数。 六、在测试环境中如何验证修复有效 验证应围绕边界行为设计,而不是只用一个正常公网 URL 测试。可以在隔离网络中部署一个简单的模拟目标服务,并准备以下测试类别: 合法的 HTTPS 公网测试域名,以及业务允许的固定端口。 回环、私有、链路本地、IPv6 和 IPv4 映射形式的地址。 带用户名密码的 URL、异常端口、空主机和解析失败的域名。 返回 3xx 的模拟服务,用于确认重定向目标会被再次校验。 解析到多个地址的域名,用于确认所有结果都经过检查。 慢响应和超大响应,用于确认超时、大小限制和资源释放逻辑。 自动化测试应同时检查业务结果和审计日志。例如,拒绝请求时应返回统一错误,不泄露底层 DNS、连接库或内部地址;日志则应能够说明请求被哪条策略拒绝。修复后还要通过依赖升级、配置检查和网络连通性测试确认没有旁路路径。 七、事件复盘应关注什么 如果已经发现疑似 SSRF,不要只把某个参数加入黑名单。复盘时应回答四个问题: 入口在哪里:是公开接口、后台功能、第三方回调,还是异步任务? 能力有多大:能访问哪些协议、端口、地址段,是否支持重定向和长响应? 影响如何确认:哪些内部服务收到过请求,是否出现异常认证、配置读取或管理操作? 为什么没有及时发现:是否缺少出口日志、异常请求告警、网络隔离或安全测试用例? 处置阶段应先保留应用访问日志、代理日志、DNS 查询记录和相关时间窗口内的网络流量摘要,再按最小影响原则收紧出口策略、轮换可能暴露的凭据,并检查内部服务的认证和访问日志。对于无法确认影响范围的情况,应以时间线和证据链为基础进行判断,避免只依赖应用层返回结果。 八、总结 SSRF 防护的核心不是记住几个内网 IP,而是建立一条完整的控制链:正确解析 URL,限制协议和端口,检查所有 DNS 结果,控制重定向,在实际连接时固定经过校验的地址,并通过代理和网络分段降低单点失误的影响。 从审计角度看,最有效的方法是沿数据流确认“用户输入如何到达网络连接”,再结合代码、配置、容器网络和日志进行验证。只有把应用校验、出口控制、监控告警和事件响应放在一起,才能让修复从一次性补丁变成可持续的安全能力。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不是 并不是 让服 务器 器替 替客 客户 户端 端发 发一 一个 让服务 服务器 务器替 器替客 替客户 客户端 户端发 端发一 发一个 让服务器 服务器替 务器替客 器替客户 替客户端 客户端发 户端发一 端发一个 这么 么简 简单 这么简 么简单 这么简单 真正 正难 难处 处理 理的 的地 地方 方在 在于 真正难 正难处 难处理 处理的 理的地 的地方 地方在 方在于 真正难处 正难处理 难处理的 处理的地 理的地方 的地方在 地方在于 解析 析存 存在 在差 差异 解析存 析存在 存在差 在差异 解析存在 析存在差 存在差异 析结 结果 果可 可能 能变 变化 解析结 析结果 结果可 果可能 可能变 能变化 解析结果 析结果可 结果可能 果可能变 可能变化 重定 定向 向会 会改 改变 变目 目标 重定向 定向会 向会改 会改变 改变目 变目标 重定向会 定向会改 向会改变 会改变目 改变目标 以及 及应 应用 用所 所在 在网 网络 络通 通常 常比 比普 普通 通用 用户 户拥 拥有 有更 更高 高的 的访 访问 问权 权限 以及应 及应用 应用所 用所在 所在网 在网络 网络通 络通常 通常比 常比普 比普通 普通用 通用户 用户拥 户拥有 拥有更 有更高 更高的 高的访 的访问 访问权 问权限 以及应用 及应用所 应用所在 用所在网 所在网络 在网络通 网络通常 络通常比 通常比普 常比普通 比普通用 普通用户 通用户拥 用户拥有 户拥有更 拥有更高 有更高的 更高的访 高的访问 的访问权 访问权限 很多 多实 实际 际事 事件 件并 并非 非由 由单 单一 一漏 漏洞 洞触 触发 很多实 多实际 实际事 际事件 事件并 件并非 并非由 非由单 由单一 单一漏 一漏洞 漏洞触 洞触发 很多实际 多实际事 实际事件 际事件并 事件并非 件并非由 并非由单 非由单一 由单一漏 单一漏洞 一漏洞触 漏洞触发 而是 是由 而是由 任意 求能 能力 请求能 求能力 请求能力 缺少 少出 出口 口控 控制 缺少出 少出口 出口控 口控制 缺少出口 少出口控 出口控制 内部 部服 务认 认证 证薄 薄弱 内部服 部服务 服务认 务认证 认证薄 证薄弱 内部服务 部服务认 服务认证 务认证薄 认证薄弱 共同 同造 造成 共同造 同造成 共同造成 问题 题场 场景 景与 与风 风险 险边 边界 问题场 题场景 场景与 景与风 与风险 风险边 险边界 问题场景 题场景与 场景与风 景与风险 与风险边 风险边界 常见 见业 业务 务功 功能 能包 包括 括图 图片 片抓 抓取 常见业 见业务 业务功 务功能 功能包 能包括 包括图 括图片 图片抓 片抓取 常见业务 见业务功 业务功能 务功能包 功能包括 能包括图 包括图片 括图片抓 图片抓取 远程 程文 文件 件导 导入 远程文 程文件 文件导 件导入 远程文件 程文件导 文件导入 测试 在线 线网 网页 页预 预览 在线网 线网页 网页预 页预览 在线网页 线网页预 网页预览 转换 换和 和第 第三 三方 方接 接口 口代 代理 转换和 换和第 和第三 第三方 三方接 方接口 接口代 口代理 转换和第 换和第三 和第三方 第三方接 三方接口 方接口代 接口代理 这些 些功 能本 本身 身并 不等 等于 于漏 这些功 些功能 功能本 能本身 本身并 身并不 并不等 不等于 等于漏 于漏洞 这些功能 些功能本 功能本身 能本身并 本身并不 身并不等 并不等于 不等于漏 等于漏洞 关键 键在 于服 端是 是否 否允 允许 许用 户完 完全 全控 制请 求目 关键在 键在于 在于服 于服务 务端是 端是否 是否允 否允许 允许用 许用户 用户完 户完全 完全控 全控制 控制请 制请求 请求目 求目标 关键在于 键在于服 在于服务 于服务端 服务端是 务端是否 端是否允 是否允许 否允许用 允许用户 许用户完 用户完全 户完全控 完全控制 全控制请 控制请求 制请求目 请求目标 及请 求是 否能 能够 够访 问不 不应 应暴 暴露 露的 的网 络区 区域 以及请 及请求 请求是 求是否 是否能 否能够 能够访 够访问 访问不 问不应 不应暴 应暴露 暴露的 露的网 的网络 网络区 络区域 以及请求 及请求是 请求是否 求是否能 是否能够 否能够访 能够访问 够访问不 访问不应 问不应暴 不应暴露 应暴露的 暴露的网 露的网络 的网络区 网络区域 用服 器访 问本 本机 机回 回环 环地 地址
  5. 背景Spring 生态里 SpEL(Spring Expression Language)用得很广,常见于权限表达式、配置解析、缓存 Key 生成、规则引擎、动态模板等场景。它本身不是漏洞,问题通常出在“把外部可控输入当作表达式执行”。这类问题在代码审计中不算高频,但一旦出现在关键路径,影响面会比较大。 这篇文章整理一次比较典型的审计思路:如何从代码里识别 SpEL 注入风险,如何做安全验证,如何判断真实影响,以及如何修复。重点放在防守和工程落地,不讨论恶意利用链。 问题场景某业务系统提供了一个“自定义规则”能力,管理员可以配置规则表达式,用于决定消息是否进入后续流程。最初设计时只考虑了内部人员使用,后来配置入口被接入到运营后台,并且部分字段可以由上游接口间接写入。 审计时看到类似代码: ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("user", user); context.setVariable("order", order); Expression expression = parser.parseExpression(ruleExpression); Boolean matched = expression.getValue(context, Boolean.class);这里的核心风险点是:ruleExpression 如果来自不可信输入,就相当于允许外部用户控制被解析执行的表达式。即使业务上认为“只有后台能配”,也需要继续确认后台权限边界、接口鉴权、配置来源、导入功能、同步任务等是否可能扩大输入面。 技术分析SpEL 的能力比较强,除了基础的属性访问、逻辑判断、集合操作外,还支持类型引用、方法调用、构造对象等能力。对于业务规则来说,通常只需要很小一部分表达式能力,例如: #user.level >= 3 and #order.amount > 100但如果使用默认解析器和默认上下文,表达式能力往往超过业务所需。安全审计时,可以从以下几个角度判断风险。 一、输入是否可控表达式是否来自 HTTP 请求参数、JSON 字段、Header、Cookie。是否来自数据库配置,而配置是否可由用户、运营后台、导入任务修改。是否来自消息队列、定时任务同步、第三方回调数据。是否存在“低权限用户影响高权限规则执行”的路径。很多漏洞不是直接把请求参数传进 parseExpression,而是经过数据库、缓存、配置中心中转后再执行。审计时不要只看 Controller,要把数据流追到最终执行点。 二、表达式执行上下文是否过宽以下写法需要重点关注: StandardEvaluationContext context = new StandardEvaluationContext(rootObject);StandardEvaluationContext 功能完整,适合可信表达式场景。如果表达式来源不可信,更建议使用能力受限的上下文,例如 SimpleEvaluationContext,并显式限制可访问对象。 常见风险点包括: 允许访问任意 Bean 或类型。允许调用对象上的任意 public 方法。上下文中放入了过多业务对象,例如用户、订单、环境配置、服务类实例。表达式执行前未做白名单校验。三、是否存在危险解析入口代码审计时可以先做关键词检索,定位 SpEL 使用点: grep -R "parseExpression" -n src/main/java grep -R "SpelExpressionParser" -n src/main/java grep -R "StandardEvaluationContext" -n src/main/java grep -R "@Value" -n src/main/java如果项目较大,可以配合 semgrep 写一个粗略规则先扫一遍: rules: - id: java-spring-spel-parse-expression message: Review SpEL parseExpression usage with external input languages: [java] severity: WARNING patterns: - pattern: | $P.parseExpression($EXPR)这类规则只能帮助定位,不等于确认漏洞。确认时要继续分析 $EXPR 的来源、是否经过限制、上下文能力以及调用场景。 安全验证方法验证时建议使用无副作用表达式,不要使用系统命令、网络请求、文件读写等破坏性动作。目标是证明“外部输入被当作表达式执行”,而不是追求攻击效果。 例如可以使用简单算术表达式验证: 1 + 2如果业务期望表达式返回布尔值,可以用: 1 + 2 == 3如果上下文中存在变量,可以验证变量访问能力: #user.level >= 1验证时记录三类信息即可: 输入点:表达式从哪里进入系统。执行点:代码中哪个位置执行了 parseExpression 和 getValue。影响范围:表达式能访问哪些变量、对象和方法。建议在测试环境完成验证,并使用最小权限账号。生产环境只做代码路径确认和日志侧佐证,不建议直接投递测试表达式。关键步骤1. 梳理数据流从执行点向上追踪 ruleExpression 的来源。如果表达式来自数据库,需要继续找写入表字段的接口、后台页面、导入功能和同步任务。 // 执行点 Expression expression = parser.parseExpression(ruleExpression); Boolean result = expression.getValue(context, Boolean.class);实际审计中经常会遇到这种情况:开发认为字段只由管理员配置,但后台接口缺少细粒度权限,或者存在批量导入接口可以由普通运营角色调用。此时漏洞影响就不再局限于管理员误操作。 2. 判断表达式能力边界重点看是否使用 StandardEvaluationContext,以及是否注册了额外能力: context.setBeanResolver(beanResolver); context.setTypeLocator(typeLocator); context.setMethodResolvers(methodResolvers); context.setPropertyAccessors(propertyAccessors);这些扩展点本身是正常功能,但在不可信表达式场景下会显著扩大表达式能力。一般来说,业务规则只需要读取少量字段并做逻辑比较,不需要 Bean 访问、类型定位和任意方法调用。 3. 使用白名单表达式模型修复时不要依赖黑名单过滤特殊字符串。黑名单很容易漏掉语法变体,也不利于长期维护。更稳妥的做法是收敛表达式语法和上下文能力。 示例:仅允许读取指定属性并做简单比较。 SimpleEvaluationContext context = SimpleEvaluationContext .forReadOnlyDataBinding() .build(); Map<String, Object> root = new HashMap<>(); root.put("level", user.getLevel()); root.put("amount", order.getAmount()); ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(ruleExpression); Boolean matched = expression.getValue(context, root, Boolean.class);这里仍然需要限制 ruleExpression 的来源和语法。对于高风险业务,更建议不要直接暴露 SpEL,而是设计 DSL 或固定条件组合,例如字段、操作符、值三元组: { "field": "amount", "operator": "GT", "value": 100 }后端将其转换为受控判断逻辑,而不是执行用户输入的表达式。 4. 增加表达式预校验如果历史原因必须继续兼容 SpEL,可以在解析前加入白名单校验,例如限制允许的变量名、操作符和字面量类型。示例思路: 只允许 #user.level、#order.amount 等固定字段。只允许比较运算、逻辑与或、括号。禁止类型引用、构造对象、方法调用、Bean 引用。限制表达式长度和嵌套深度。工程上可以将表达式解析为 AST 后检查节点类型,而不是简单正则。正则适合做第一层粗过滤,不适合作为唯一防线。 5. 加日志和审计规则表达式属于高风险配置,建议记录完整变更审计: 谁修改了表达式。修改前后内容。来源 IP 和操作入口。审批单或变更原因。规则命中后的关键业务动作。同时对异常表达式解析失败、频繁变更、非常规字符等行为做告警。很多时候,日志能帮助区分误配置、测试行为和真实风险。 常见误区误区一:只有管理员能配,所以没风险后台权限也需要分层。管理员账号可能被钓鱼、共享、误授权,后台接口也可能存在越权。只要表达式能力超过业务需要,就应该收敛。 误区二:过滤几个关键字就够了表达式语言通常存在多种等价写法,黑名单很难覆盖完整。正确方向是白名单、最小权限上下文、减少暴露对象。 误区三:测试没弹计算器,所以不是漏洞验证漏洞不应以破坏性效果为目标。只要能证明不可信输入被作为表达式执行,并且表达式能力超出预期,就应按风险处理。 误区四:只修入口,不修执行点入口可能不止一个。修复应优先放在执行点附近,形成统一安全封装,避免后续新入口绕过。 修复建议措施说明替换为 DSL优先推荐。将规则表达为字段、操作符和值,由后端受控执行。使用 SimpleEvaluationContext减少表达式能力,避免默认开放过多访问能力。白名单字段只暴露业务必需字段,不传入完整对象或服务实例。限制语法禁止方法调用、类型引用、构造对象、Bean 访问等非必要能力。配置审计记录表达式变更、审批、执行异常和命中情况。统一封装不要在业务代码里到处直接 new SpelExpressionParser,应封装为统一安全组件。一个相对稳妥的封装思路可以将表达式执行集中到一个组件中,统一做长度限制、变量白名单、上下文收敛和异常处理。 public class SafeRuleEvaluator { private static final int MAX_EXPR_LENGTH = 200; private final ExpressionParser parser = new SpelExpressionParser(); private final SimpleEvaluationContext context = SimpleEvaluationContext .forReadOnlyDataBinding() .build(); public boolean evaluate(String expression, Map<String, Object> variables) { if (expression == null || expression.length() > MAX_EXPR_LENGTH) { throw new IllegalArgumentException("invalid expression length"); } if (!isAllowedExpression(expression)) { throw new IllegalArgumentException("expression is not allowed"); } Map<String, Object> safeVariables = filterVariables(variables); Expression exp = parser.parseExpression(expression); Boolean result = exp.getValue(context, safeVariables, Boolean.class); return Boolean.TRUE.equals(result); } private boolean isAllowedExpression(String expression) { // 示例:实际项目中建议基于 AST 做白名单校验 return !expression.contains("T(") && !expression.contains("new ") && !expression.contains("@") && !expression.contains(".class"); } private Map<String, Object> filterVariables(Map<String, Object> variables) { Map<String, Object> safe = new HashMap<>(); safe.put("level", variables.get("level")); safe.put("amount", variables.get("amount")); safe.put("region", variables.get("region")); return safe; } }上面只是示例,不建议把字符串过滤当作最终方案。更可靠的方式是从产品形态上减少表达式自由度,例如改成固定条件配置。 排查清单项目中是否存在 SpelExpressionParser、parseExpression、getValue。表达式参数是否可能来自用户输入、后台配置、导入文件、消息队列。是否使用 StandardEvaluationContext。上下文中是否放入完整业务对象、服务对象、环境对象。是否注册 BeanResolver、TypeLocator、MethodResolver。是否有表达式长度、语法、字段白名单限制。是否有配置变更审计和异常告警。是否存在多个入口写入同一表达式字段。总结SpEL 注入的本质不是 Spring 有问题,而是把不可信输入交给了能力过强的表达式引擎。审计这类问题时,不要只盯着单个接口,要把“输入来源、存储中转、执行上下文、表达式能力、业务影响”串起来看。 修复上优先选择 DSL 或固定规则模型,其次才是受限 SpEL。无论采用哪种方案,都应遵循最小能力原则:只暴露必要字段,只允许必要语法,只在必要场景执行,并留下可追溯的配置审计。这样即使后续业务扩展,也不容易把一个规则功能演变成高风险执行入口。 代码审计、调用链与关键函数定位示意 背景 生态 态里 生态里 用得 得很 很广 用得很 得很广 用得很广 常见 见于 于权 权限 限表 表达 达式 常见于 见于权 于权限 权限表 限表达 表达式 常见于权 见于权限 于权限表 权限表达 限表达式 配置 置解 解析 配置解 置解析 配置解析 缓存 生成 规则 则引 引擎 规则引 则引擎 规则引擎 动态 态模 模板 板等 等场 场景 动态模 态模板 模板等 板等场 等场景 动态模板 态模板等 模板等场 板等场景 它本 本身 身不 不是 是漏 漏洞 它本身 本身不 身不是 不是漏 是漏洞 它本身不 本身不是 身不是漏 不是漏洞 问题 题通 通常 常出 出在 问题通 题通常 通常出 常出在 问题通常 题通常出 通常出在 把外 外部 部可 可控 控输 输入 入当 当作 作表 式执 执行 把外部 外部可 部可控 可控输 控输入 输入当 入当作 当作表 作表达 达式执 式执行 把外部可 外部可控 部可控输 可控输入 控输入当 输入当作 入当作表 当作表达 作表达式 表达式执 达式执行 这类 类问 题在 在代 代码 码审 审计 计中 中不 不算 算高 高频 这类问 类问题 问题在 题在代 在代码 代码审 码审计 审计中 计中不 中不算 不算高 算高频 这类问题 类问题在 问题在代 题在代码 在代码审 代码审计 码审计中 审计中不 计中不算 中不算高 不算高频 但一 一旦 旦出 出现 现在 在关 关键 键路 路径 但一旦 一旦出 旦出现 出现在 现在关 在关键 关键路 键路径 但一旦出 一旦出现 旦出现在 出现在关 现在关键 在关键路 关键路径 影响 响面 面会 会比 比较 较大 影响面 响面会 面会比 会比较 比较大 影响面会 响面会比 面会比较 会比较大 这篇 篇文 文章 章整 整理 理一 一次 次比 较典 典型 型的 的审 计思 思路 这篇文 篇文章 文章整 章整理 整理一 理一次 一次比 次比较 比较典 较典型 典型的 型的审 的审计 审计思 计思路 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次比 一次比较 次比较典 比较典型 较典型的 典型的审 型的审计 的审计思 审计思路 如何 何从 从代 码里 里识 识别 如何从 何从代 从代码 代码里 码里识 里识别 如何从代 何从代码 从代码里 代码里识 码里识别 注入 入风 风险 注入风 入风险 注入风险 何做 做安 安全 全验 验证 如何做 何做安 做安全 安全验 全验证 如何做安 何做安全 做安全验 安全验证 何判 判断 断真 真实 实影 如何判 何判断 判断真 断真实 真实影 实影响 如何判断 何判断真 判断真实 断真实影 真实影响 以及 及如 何修 修复 以及如 及如何 如何修 何修复 以及如何 及如何修 如何修复 重点 点放 放在 在防 防守 守和 和工 工程 程落 落地 重点放 点放在 放在防 在防守 防守和 守和工 和工程 工程落 程落地 重点放在 点放在防 放在防守 在防守和 防守和工 守和工程 和工程落 工程落地 不讨 讨论 论恶 恶意 意利 利用 用链 不讨论 讨论恶 论恶意 恶意利 意利用 利用链 不讨论恶 讨论恶意 论恶意利 恶意利用 意利用链 题场 景某 某业 业务 务系 系统 统提 提供 供了 了一 一个 问题场 题场景 场景某 景某业 某业务 业务系 务系统 系统提 统提供 提供了 供了一 了一个 问题场景 题场景某 场景某业 景某业务 某业务系 业务系统 务系统提 系统提供 统提供了 提供了一 供了一个 自定 定义 义规 自定义 定义规 义规则 自定义规 定义规则 能力 管理 理员 员可 可以 以配 置规 则表 管理员 理员可 员可以 可以配 以配置 配置规 置规则 规则表 则表达 管理员可 理员可以 员可以配 可以配置 以配置规 配置规则 置规则表 规则表达 则表达式 用于 于决 决定 定消 消息 息是 是否 否进 进入 入后 后续 续流 流程 用于决 于决定 决定消 定消息 消息是 息是否 是否进 否进入 进入后 入后续 后续流 续流程 用于决定 于决定消 决定消息 定消息是 消息是否 息是否进 是否进入 否进入后 进入后续 入后续流 后续流程 最初 初设 设计 计时 时只 只考 考虑 虑了 了内 内部 部人 人员 员使 使用 最初设 初设计 设计时 计时只 时只考 只考虑 考虑了 虑了内 了内部 内部人 部人员 人员使 员使用 最初设计 初设计时 设计时只 计时只考 时只考虑 只考虑了 考虑了内 虑了内部 了内部人 内部人员 部人员使 人员使用 后来 来配 置入 入口 口被 被接 接入 入到 到运 运营 营后 后台 后来配 来配置
  6. SSRF(服务端请求伪造)在实际项目中并不总是以一个明显的“URL 参数直接传给 HTTP 客户端”形式出现。更常见的情况是:业务允许导入远程图片、解析 Webhook、抓取网页标题,或者通过内部服务生成预览;随着代码分层和功能迭代,用户输入可能经过多次拼接、解码和重定向,最终形成一条不容易被发现的服务端请求链路。 这类问题的风险也不能只看“能不能访问内网”。真正需要评估的是请求发起位置、解析和连接过程、重定向行为、响应处理方式,以及云环境中的凭证和管理接口是否暴露。本文以一次典型的代码审计为例,整理一套偏工程化的发现、复现、修复和验证方法。示例均使用本地测试环境,不涉及对第三方系统的探测。 一、问题场景:图片预览功能为何容易出问题某内容系统提供远程图片预览功能,接口接收一个图片地址,服务端下载图片后生成缩略图。初始实现大致如下: def fetch_image(url: str) -> bytes: response = requests.get(url, timeout=5) response.raise_for_status() return response.content从业务角度看,这段代码很直观,但安全边界几乎没有建立: 协议没有限制,调用方可能尝试非 HTTP 协议处理器。域名解析结果没有校验,域名可能解析到本机、内网或链路本地地址。即使首次解析结果安全,连接前再次解析时也可能发生地址变化。默认跟随重定向,初始地址安全并不代表最终地址安全。没有限制响应大小,可能造成内存或磁盘资源耗尽。如果异常信息直接返回给前端,可能泄露内部地址、连接细节或响应内容。二、审计重点:不要只搜索一个函数审计 SSRF 时,建议从“输入源—规范化—请求构造—网络连接—响应处理”五个环节建立数据流,而不是只搜索 requests.get 或 HttpClient.execute。 1. 识别外部可控输入重点检查以下输入来源: JSON、表单、GraphQL 和 RPC 请求中的 URL、回调地址、图片地址。导入文件中的远程引用,例如 Markdown、RSS 或配置文件。数据库中由用户创建的 Webhook、通知地址和头像地址。消息队列、定时任务或管理后台中的抓取任务参数。一旦发现 URL 被保存到数据库,不能认为风险已经结束。持久化字段往往会在后续异步任务中被重新取出并请求,审计范围应覆盖同步接口、队列消费者和定时任务。 2. 追踪请求封装层实际项目通常有统一的网络访问封装,例如 download、fetch、proxy、preview 等方法。建议沿调用链向下追踪,重点确认: 是否允许调用方覆盖代理、请求头和超时时间。是否对重定向后的每一跳重新执行安全校验。是否先解析域名再建立连接,还是由底层库自动解析。是否把响应内容交给 XML、图片、压缩包等二次解析器。是否将响应头、状态码和异常原文返回给外部调用方。3. 关注“看起来不是 URL”的输入有些危险输入不一定直接叫 url。例如 host、endpoint、callback、source、avatar、image、feed、webhook 等字段,经过拼接后可能构成完整请求地址。对这类字段,应该结合数据流和实际调用点判断,而不能只依赖变量命名。 三、地址校验的关键:校验最终连接目标,而不是只校验字符串常见的错误做法是使用字符串前缀判断,例如判断地址是否以某个域名开头。这种方式无法可靠处理用户信息、端口、大小写、尾随点、IPv6、域名解析和重定向等情况。 较稳妥的策略通常包括以下步骤: 只接受明确允许的协议,例如 http 和 https。解析 URL,要求存在主机名,拒绝用户名、密码和不必要的自定义端口。对主机名做规范化处理,避免大小写、尾随点和国际化域名造成判断差异。解析所有 A 和 AAAA 记录,对每个地址执行内网、回环、链路本地、组播和保留地址检查。实际连接时绑定经过校验的 IP,同时保留正确的 Host 和 TLS SNI,避免重新解析到其他地址。禁用或接管自动重定向,并对每次跳转重新执行完整校验。下面是一个用于本地服务的简化校验示例。它表达的是安全边界和测试思路,生产环境还需要结合网络库的连接绑定能力实现完整的 DNS 重绑定防护。 from ipaddress import ip_address from urllib.parse import urlparse import socket ALLOWED_SCHEMES = {"http", "https"} def is_public_address(value: str) -> bool: address = ip_address(value) return not ( address.is_private or address.is_loopback or address.is_link_local or address.is_multicast or address.is_reserved or address.is_unspecified ) def validate_target(raw_url: str) -> tuple[str, int]: parsed = urlparse(raw_url) if parsed.scheme.lower() not in ALLOWED_SCHEMES: raise ValueError("unsupported scheme") if not parsed.hostname or parsed.username or parsed.password: raise ValueError("invalid authority") host = parsed.hostname.rstrip(".").lower() port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80) addresses = {item[4][0] for item in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)} if not addresses or not all(is_public_address(item) for item in addresses): raise ValueError("target address is not allowed") return host, port这里有一个容易被忽略的细节:只检查 DNS 查询结果仍然不一定足够。若 HTTP 客户端稍后根据域名重新解析,攻击者控制的 DNS 服务可能返回不同地址。因此生产实现应选择支持“预解析地址并绑定连接”的客户端,或者在网络层增加出口代理和目的地址策略,避免应用层单独承担全部风险。 四、重定向、解析差异与协议处理1. 重定向必须逐跳校验很多 HTTP 客户端默认自动跟随 301、302、303 和 307。若第一跳是公开地址,响应再跳转到受限地址,单次入口校验就会失效。建议默认关闭自动重定向;如果业务确实需要跟随,应限制最大跳数,并对 Location 解析、协议、主机、端口和解析结果逐跳校验。 def fetch_with_policy(url: str, session) -> bytes: current = url for _ in range(3): validate_target(current) response = session.get( current, allow_redirects=False, timeout=(3, 5), stream=True, ) if response.status_code not in {301, 302, 303, 307, 308}: return read_limited_body(response, limit=5 * 1024 * 1024) location = response.headers.get("Location") if not location: raise ValueError("redirect location is empty") current = urljoin(current, location) raise ValueError("too many redirects")上面的示例省略了导入和响应体读取函数,重点是控制重定向边界。还需要注意:不同语言和库对 URL 解析、反斜杠、编码字符、空白字符以及 IPv6 表示法的处理可能不同,校验器与实际 HTTP 客户端最好使用同一套规范化逻辑,避免“校验看到的地址”和“连接实际使用的地址”不一致。 2. 不要把非 HTTP 协议交给通用请求组件如果业务只需要获取网页或图片,就不应让通用组件处理 file、ftp、gopher 等协议。某些库还支持自定义协议处理器,或者会根据输入自动选择不同的适配器。限制协议只是第一步,最好在依赖配置和代码层同时确认不加载无关协议处理能力。 五、响应处理同样是攻击面SSRF 风险不仅在于“请求发出去了什么”,也在于“服务端拿回来后做了什么”。审计时应继续检查以下链路: 响应是否被完整读入内存,是否有最大响应体限制。Content-Type 是否可信,是否会根据文件内容自动识别类型。图片、XML、PDF、压缩包等解析器是否存在二次解析风险。响应是否被原样返回,是否可能成为内部服务内容代理。是否把上游响应头中的 Location、Set-Cookie 或缓存字段传递给客户端。一个相对稳妥的下载流程应采用流式读取、大小上限和内容类型白名单。例如图片预览只允许 image/jpeg、image/png 等有限类型,并在服务端重新编码后再输出,不直接返回上游字节流。 def read_limited_body(response, limit: int) -> bytes: total = 0 chunks = [] for chunk in response.iter_content(chunk_size=8192): if not chunk: continue total += len(chunk) if total > limit: raise ValueError("response body is too large") chunks.append(chunk) return b"".join(chunks)六、网络层防护:不要只依赖应用代码应用层校验适合表达业务允许访问的范围,但无法替代网络隔离。生产环境中建议同时落实以下措施: 将抓取服务放在独立的网络分区,默认禁止访问数据库、缓存、消息队列和管理平面。通过出口代理统一执行域名、端口、目的地址和审计策略。限制应用运行身份的云平台权限,不让普通业务实例读取高价值实例凭证。在防火墙或安全组层面拒绝回环、链路本地、私有网段和云管理地址。对外联请求记录请求任务、规范化主机、解析地址、最终状态和失败原因,但避免记录敏感查询参数。日志中的地址信息应经过脱敏和权限控制。安全团队需要能够根据任务 ID 还原完整请求链路,同时避免把内部拓扑、凭证参数或上游响应直接写入普通业务日志。 七、验证方法:建立可重复的本地测试修复后不要只通过一次接口调用判断问题已经解决。可以在本地搭建几个隔离测试服务,覆盖不同的边界条件: 公开地址直接访问,应符合业务预期。解析到回环、私有网段、链路本地和 IPv6 保留地址时,应在连接前失败。公开地址重定向到受限地址时,应在第二跳被拒绝。多次解析返回不同地址时,应保持连接目标与校验结果一致。响应体超过限制时,应中止读取并释放连接。非允许协议、缺失主机、带用户信息或异常端口的 URL,应统一拒绝。测试重点是验证“是否建立了连接”,而不只是观察接口返回码。可以在隔离网络中使用一个仅用于测试的监听服务记录连接来源和 Host,不要将测试请求指向真实内网系统。 def test_redirect_to_private_target_is_rejected(client, redirect_server): public_url = redirect_server.url_for(" result = client.preview({"url": public_url}) assert result.status_code == 400 assert redirect_server.connection_count == 1 除单元测试外,还应加入静态检查和集成测试。例如在代码审查中搜索未经统一封装的网络请求调用: rg 'requests\.(get|post|request)|urlopen|HttpClient|WebClient' src/搜索结果不能直接等同于漏洞,但可以帮助发现绕过统一安全组件的新增代码。对于 Java、Go、Node.js 等技术栈,应根据项目实际客户端补充对应规则,并把检查接入代码审查或持续集成流程。 八、常见修复误区只做字符串黑名单:黑名单容易遗漏地址表示形式,也无法处理解析和重定向。只禁止常见私网段:还应考虑回环、链路本地、组播、保留地址和 IPv6。只校验第一次请求:重定向和多阶段抓取会产生新的目标。只在前端校验:前端校验可以改善交互,但不能作为服务端安全边界。修复后仍允许任意代理设置:攻击者可能通过代理改变实际请求路径。错误信息过于详细:应对外返回稳定的业务错误码,对内记录足够的诊断信息。九、结论SSRF 的治理核心不是增加一个 URL 黑名单,而是把“服务端是否允许访问某个网络目标”变成明确、可审计、可测试的策略。一次完整的修复应同时覆盖输入源追踪、协议限制、主机解析、连接绑定、重定向控制、响应大小、网络隔离和日志审计。 从代码审计角度看,最有价值的证据不是某个请求函数的存在,而是一条能够闭合的数据流:外部输入如何进入任务,经过哪些规范化和校验,最终连接到哪个地址,响应又被哪些组件继续处理。只要围绕这条链路建立测试和防护,类似的远程抓取、Webhook、文件导入和预览功能就能复用同一套安全基线,减少问题在不同业务模块中反复出现。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 在实 实际 际项 项目 目中 中并 并不 不总 总是 是以 以一 一个 个明 明显 显的 在实际 实际项 际项目 项目中 目中并 中并不 并不总 不总是 总是以 是以一 以一个 一个明 个明显 明显的 在实际项 实际项目 际项目中 项目中并 目中并不 中并不总 并不总是 不总是以 总是以一 是以一个 以一个明 一个明显 个明显的 参数 数直 直接 接传 传给 参数直 数直接 直接传 接传给 参数直接 数直接传 直接传给 客户 户端 客户端 形式 式出 出现 形式出 式出现 形式出现 更常 常见 见的 的情 情况 况是 更常见 常见的 见的情 的情况 情况是 更常见的 常见的情 见的情况 的情况是 业务 务允 允许 许导 导入 入远 远程 程图 图片 业务允 务允许 允许导 许导入 导入远 入远程 远程图 程图片 业务允许 务允许导 允许导入 许导入远 导入远程 入远程图 远程图片 解析 抓取 取网 网页 页标 标题 抓取网 取网页 网页标 页标题 抓取网页 取网页标 网页标题 或者 者通 通过 过内 内部 部服 务生 生成 成预 预览 或者通 者通过 通过内 过内部 内部服 部服务 服务生 务生成 生成预 成预览 或者通过 者通过内 通过内部 过内部服 内部服务 部服务生 服务生成 务生成预 生成预览 随着 着代 代码 码分 分层 层和 和功 功能 能迭 迭代 随着代 着代码 代码分 码分层 分层和 层和功 和功能 功能迭 能迭代 随着代码 着代码分 代码分层 码分层和 分层和功 层和功能 和功能迭 功能迭代 用户 户输 输入 入可 可能 能经 经过 过多 多次 次拼 拼接 用户输 户输入 输入可 入可能 可能经 能经过 经过多 过多次 多次拼 次拼接 用户输入 户输入可 输入可能 入可能经 可能经过 能经过多 经过多次 过多次拼 多次拼接 解码 码和 和重 重定 定向 解码和 码和重 和重定 重定向 解码和重 码和重定 和重定向 最终 终形 形成 成一 一条 条不 不容 容易 易被 被发 发现 现的 的服 求链 链路 最终形 终形成 形成一 成一条 一条不 条不容 不容易 容易被 易被发 被发现 发现的 现的服 的服务 请求链 求链路 最终形成 终形成一 形成一条 成一条不 一条不容 条不容易 不容易被 容易被发 易被发现 被发现的 发现的服 现的服务 的服务端 端请求链 请求链路 这类 类问 问题 题的 的风 风险 险也 也不 不能 能只 只看 这类问 类问题 问题的 题的风 的风险 风险也 险也不 也不能 不能只 能只看 这类问题 类问题的 问题的风 题的风险 的风险也 风险也不 险也不能 也不能只 不能只看 能不 能访 访问 问内 内网 能不能 不能访 能访问 访问内 问内网 能不能访 不能访问 能访问内 访问内网 真正 正需 需要 要评 评估 估的 的是 是请 求发 发起 起位 位置 真正需 正需要 需要评 要评估 评估的 估的是 的是请 是请求 请求发 求发起 发起位 起位置 真正需要 正需要评 需要评估 要评估的 评估的是 估的是请 的是请求 是请求发 请求发起 求发起位 发起位置 析和 和连 连接 接过 过程 解析和 析和连 和连接 连接过 接过程 解析和连 析和连接 和连接过 连接过程 向行 行为 定向行 向行为 重定向行 定向行为 响应 应处 处理 理方 方式 响应处 应处理 处理方 理方式 响应处理 应处理方 处理方式 以及 及云 云环 环境 境中 中的 的凭 凭证 证和 和管 管理 理接 接口 口是 是否 否暴 暴露 以及云 及云环 云环境 环境中 境中的 中的凭 的凭证 凭证和 证和管 和管理 管理接 理接口 接口是 口是否 是否暴 否暴露 以及云环 及云环境 云环境中 环境中的 境中的凭 中的凭证 的凭证和 凭证和管 证和管理 和管理接 管理接口 理接口是 接口是否 口是否暴 是否暴露 本文 文以 一次 次典 典型 型的 的代 码审 审计 计为 为例 本文以 文以一 以一次 一次典 次典型 典型的 型的代 的代码 代码审 码审计 审计为 计为例 本文以一 文以一次 以一次典 一次典型 次典型的 典型的代 型的代码 的代码审 代码审计 码审计为 审计为例 整理 理一 一套 套偏 偏工 工程 程化 化的 的发 整理一 理一套 一套偏 套偏工 偏工程 工程化 程化的 化的发 的发现 整理一套 理一套偏 一套偏工 套偏工程 偏工程化 工程化的 程化的发 化的发现 复现 修复 复和 和验 验证 证方 方法 修复和
  7. SSRF(服务端请求伪造)经常出现在“根据 URL 抓取内容”“图片远程转存”“Webhook 测试”“网页截图”等功能中。它的危险性不只在于访问内网地址,更在于应用自身拥有网络权限、身份凭据和信任关系,一旦请求边界设计不清晰,就可能把一个普通的业务接口变成内部网络访问代理。 本文以一次典型代码审计为背景,讨论如何判断 SSRF 是否成立、如何在不触碰真实敏感系统的前提下完成验证,以及如何从 URL 解析、重定向、DNS 解析和出口控制多个层面建立防护闭环。 一、问题场景:一个看似普通的远程抓取接口目标功能是让用户提交一个图片地址,服务端下载图片并保存到对象存储。初始代码逻辑大致如下: public byte[] fetch(String url) throws IOException { URL target = new URL(url); HttpURLConnection connection = (HttpURLConnection) target.openConnection(); connection.setConnectTimeout(3000); connection.setReadTimeout(5000); connection.setInstanceFollowRedirects(true); return connection.getInputStream().readAllBytes(); }这段代码的业务意图很明确,但安全边界并不完整: 允许用户完全控制目标 URL。没有限制协议,只是通过 URL 类自动选择处理方式。没有校验目标 IP 是否属于内网、回环、链路本地或保留地址。默认跟随重定向,初始地址合法并不代表最终地址合法。没有限制响应大小,可能引入资源消耗问题。应用所在网络环境可能可以访问管理面板、服务发现接口或内部调试端点。需要注意的是,SSRF 是否能够造成实际影响,取决于应用部署位置、网络分段、身份权限和出口策略。代码存在缺陷不等于已经证明可以访问某个敏感系统,审计结论应区分“代码级风险”和“环境级影响”。 二、审计时先确认请求链路分析 SSRF 时,不要一开始就围绕某个特殊地址构造测试。更稳妥的做法是先梳理完整请求链路: 用户输入从哪里进入,是否经过 URL 解码、JSON 反序列化或模板拼接。最终使用的是哪一种 HTTP 客户端,是否自动跟随重定向。域名解析发生在应用进程、代理还是网关。是否存在代理环境变量,例如 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。响应内容是否会回显给用户,还是只保存到文件或对象存储。请求是否携带了默认 Cookie、Authorization 或云平台身份凭据。可以在测试环境中增加结构化日志,但不应记录完整敏感 URL 或认证头。推荐记录请求任务编号、目标主机的脱敏值、解析后的地址类别、最终状态码、响应大小和耗时。例如: { "event": "outbound_fetch", "request_id": "8f1c...", "host": "example.invalid", "resolved_ip_class": "public", "redirect_count": 0, "status": 200, "bytes": 18432, "duration_ms": 421 }三、验证方法:只使用自建或授权测试目标在授权测试环境中,可以准备一个外部测试站点和一个本地测试站点,用于观察请求是否从服务端发出。测试站点只返回固定文本,不包含任何敏感数据。验证重点不是读取内部信息,而是确认以下事实: 请求是否确实由服务端发起,而不是浏览器直接访问。服务端是否能访问仅对本机或内网开放的测试端口。遇到 3xx 响应时,客户端是否继续访问新的地址。DNS 解析结果变化后,客户端是否重新校验目标地址。响应大小、连接时间和重定向次数是否受到限制。一个安全的本地验证服务可以只监听测试机的回环接口,并返回固定内容: from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_GET(self): body = b"authorized-ssrf-test" self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): print(fmt % args) HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()测试完成后,应删除临时服务并核对访问日志。不要把生产环境的管理接口、容器元数据服务、数据库端口或其他内部系统作为验证目标。 四、核心风险一:URL 解析与 IP 校验不一致很多防护只做了字符串判断,例如禁止 URL 中出现 localhost 或某个内网网段。这种方式容易受到不同解析器行为差异的影响。实际审计时应关注以下问题: 是否只允许 http 和 https,是否会接受 file、ftp 或其他协议。主机名是否经过标准化处理,包括大小写、尾部点和国际化域名。校验的是原始域名,还是实际 DNS 解析后的每一个地址。IPv4、IPv6、IPv4 映射 IPv6 的处理是否一致。是否错误地把带用户名信息的 URL 当成普通主机名解析。推荐采用“解析 URL—规范化主机名—解析全部地址—逐个判断地址类别—再建立连接”的顺序,而不是先用字符串黑名单过滤。 地址判断至少应覆盖回环地址、私有地址、链路本地地址、未指定地址、组播地址、文档保留地址以及企业网络明确禁止的网段。对于 IPv6,不能只用 IPv4 的正则表达式处理。 五、核心风险二:DNS 解析与连接之间的时间差即使代码先解析域名并确认结果属于公网,也可能在真正建立连接时再次解析,导致检查结果与连接目标不一致。这类问题本质上是“检查时”和“使用时”之间存在时间窗口。 防护思路包括: 使用经过安全封装的 HTTP 客户端,在连接阶段固定已校验的地址。对每次连接和每次重定向重新执行地址校验。对域名解析结果设置合理缓存时间,避免无限信任长期结果。在网络层配置出口代理,由代理统一执行目标地址与端口策略。不要把 DNS 校验当成唯一防线,必须配合防火墙和网络分段。如果业务允许,最简单可靠的方案通常是维护目标域名白名单,并在网关层只允许访问经过审核的目标,而不是试图覆盖所有复杂的地址解析边界。 六、核心风险三:重定向绕过初始校验重定向是实际项目中最容易遗漏的路径。应用可能先校验了一个公开站点,但该站点返回 302,将客户端引向另一个未授权地址。如果客户端自动跟随,初始校验就失去了意义。 有两种常见处理方式: 默认关闭自动重定向,由业务代码读取 Location 后重新解析、校验并决定是否继续。如果必须自动跟随,则限制重定向次数,并确保客户端在每一跳都执行协议、域名、端口和 IP 校验。伪代码示例如下: response = request_once(url) for i in range(MAX_REDIRECTS): if response.status not in REDIRECT_STATUS: break next_url = resolve_location(url, response.headers["Location"]) target = parse_and_normalize(next_url) assert target.scheme in {"http", "https"} assert target.port in {80, 443} addresses = resolve_all(target.hostname) if any(is_blocked_address(addr) for addr in addresses): raise PolicyError("redirect target is not allowed") url = target.to_string() response = request_once(url)这里的关键点是:重定向目标必须被当成新的用户输入处理,而不是当成可信的服务器返回值。 七、建立一套可落地的防护策略1. 协议与端口白名单仅允许业务真正需要的协议,通常是 HTTPS,必要时再开放 HTTP。端口应采用白名单,而不是简单禁止几个常见端口。若图片转存只需要访问公网 443 端口,就没有必要允许任意端口。 2. 目标域名白名单对于合作方回调、固定图床或文档系统,优先维护精确域名白名单。白名单匹配应避免简单的后缀判断,例如允许 example.com 时,不应顺带允许 evil-example.com。可以采用规范化后完全匹配,或明确的子域名边界匹配。 3. 禁止访问敏感地址类别在应用层和出口代理层同时阻断回环、私有、链路本地、组播、未指定及企业内部保留网段。应用层负责业务策略,网络层负责最后一道边界,两者不能互相替代。 4. 限制请求规模设置连接超时、读取超时、最大响应大小、最大重定向次数和最大下载时间。读取响应时不要无条件调用 readAllBytes,应使用带上限的流式读取: MAX_BYTES = 5 * 1024 * 1024 received = 0 chunks = [] while True: chunk = stream.read(8192) if not chunk: break received += len(chunk) if received > MAX_BYTES: raise SizeLimitError("response too large") chunks.append(chunk) 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 经常 常出 出现 现在 经常出 常出现 出现在 经常出现 常出现在 根据 抓取 取内 内容 抓取内 取内容 抓取内容 图片 片远 远程 程转 转存 图片远 片远程 远程转 程转存 图片远程 片远程转 远程转存 测试 网页 页截 截图 网页截 页截图 网页截图 等功 功能 能中 等功能 功能中 等功能中 它的 的危 危险 险性 性不 不只 只在 在于 于访 访问 问内 内网 网地 地址 它的危 的危险 危险性 险性不 性不只 不只在 只在于 在于访 于访问 访问内 问内网 内网地 网地址 它的危险 的危险性 危险性不 险性不只 性不只在 不只在于 只在于访 在于访问 于访问内 访问内网 问内网地 内网地址 更在 于应 应用 用自 自身 身拥 拥有 有网 网络 络权 权限 更在于 在于应 于应用 应用自 用自身 自身拥 身拥有 拥有网 有网络 网络权 络权限 更在于应 在于应用 于应用自 应用自身 用自身拥 自身拥有 身拥有网 拥有网络 有网络权 网络权限 身份 份凭 凭据 据和 和信 信任 任关 关系 身份凭 份凭据 凭据和 据和信 和信任 信任关 任关系 身份凭据 份凭据和 凭据和信 据和信任 和信任关 信任关系 一旦 旦请 求边 边界 界设 设计 计不 不清 清晰 一旦请 旦请求 请求边 求边界 边界设 界设计 设计不 计不清 不清晰 一旦请求 旦请求边 请求边界 求边界设 边界设计 界设计不 设计不清 计不清晰 就可 可能 能把 把一 一个 个普 普通 通的 的业 业务 务接 接口 口变 变成 成内 内部 部网 络访 问代 代理 就可能 可能把 能把一 把一个 一个普 个普通 普通的 通的业 的业务 业务接 务接口 接口变 口变成 变成内 成内部 内部网 部网络 网络访 络访问 访问代 问代理 就可能把 可能把一 能把一个 把一个普 一个普通 个普通的 普通的业 通的业务 的业务接 业务接口 务接口变 接口变成 口变成内 变成内部 成内部网 内部网络 部网络访 网络访问 络访问代 访问代理 本文 文以 以一 一次 次典 典型 型代 代码 码审 审计 计为 为背 背景 本文以 文以一 以一次 一次典 次典型 典型代 型代码 代码审 码审计 审计为 计为背 为背景 本文以一 文以一次 以一次典 一次典型 次典型代 典型代码 型代码审 代码审计 码审计为 审计为背 计为背景 讨论 论如 如何 何判 判断 讨论如 论如何 如何判 何判断 讨论如何 论如何判 如何判断 是否 否成 成立 是否成 否成立 是否成立 何在 在不 不触 触碰 碰真 真实 实敏 敏感 感系 系统 统的 的前 前提 提下 下完 完成 成验 验证 如何在 何在不 在不触 不触碰 触碰真 碰真实 真实敏 实敏感 敏感系 感系统 系统的 统的前 的前提 前提下 提下完 下完成 完成验 成验证 如何在不 何在不触 在不触碰 不触碰真 触碰真实 碰真实敏 真实敏感 实敏感系 敏感系统 感系统的 系统的前 统的前提 的前提下 前提下完 提下完成 下完成验 完成验证 以及 及如 何从 以及如 及如何 如何从 以及如何 及如何从 解析 重定 定向 重定向 析和 和出 出口 口控 控制 制多 多个 个层 层面 面建 建立 立防 防护 护闭 闭环 解析和 析和出 和出口 出口控 口控制 控制多 制多个 多个层 个层面 层面建 面建立 建立防 立防护 防护闭 护闭环 解析和出 析和出口 和出口控 出口控制 口控制多 控制多个 制多个层 多个层面 个层面建 层面建立 面建立防 建立防护 立防护闭 防护闭环 问题 题场 场景 问题场 题场景 问题场景 个看 看似 似普 的远 程抓 取接 口目 目标 标功 能是 是让 让用 用户 户提 提交 交一 个图 片地 一个看 个看似 看似普 似普通 通的远 的远程 远程抓 程抓取 抓取接 取接口 接口目 口目标 目标功 标功能 功能是 能是让 是让用 让用户 用户提 户提交 提交一 交一个 一个图 个图片 图片地 片地址 一个看似 个看似普 看似普通 似普通的 普通的远 通的远程 的远程抓 远程抓取 程抓取接 抓取接口 取接口目 接口目标 口目标功 目标功能 标功能是 功能是让 能是让用 是让用户 让用户提 用户提交 户提交一 提交一个 交一个图 一个图片 个图片地 图片地址 端下 下载 载图 片并 并保 保存 存到 到对 对象 象存 存储 务端下 端下载 下载图 载图片 图片并 片并保 并保存 保存到 存到对 到对象 对象存 象存储 服务端下 务端下载 端下载图
  8. 背景很多内网 Web 系统上线时间早、业务依赖重,平时很少有人系统性审计。真正出问题时,常见情况不是单点漏洞特别复杂,而是几个普通风险叠加:老旧框架、调试接口遗留、上传目录可访问、权限校验分散、日志缺失。本文整理一次偏防守视角的代码审计与风险收敛方法,重点放在如何发现问题、验证风险边界、推动修复,不涉及未授权攻击或恶意利用。 场景假设是企业内部自研 Web 系统,技术栈为 Java Spring MVC / Spring Boot 混合,前端有少量老 JSP,部署在 Linux + Nginx + Tomcat 环境。目标不是“打穿系统”,而是通过代码审计和受控验证,把可被利用的缺陷收敛到可修复、可复测的清单。 问题与场景这类系统常见的问题并不新,但因为业务迭代多年,风险点往往藏在边角位置: 接口权限校验不一致:部分 Controller 依赖前端菜单控制,没有后端鉴权。文件上传处理粗糙:只校验后缀或 Content-Type,上传目录又能被直接访问。SQL 拼接遗留:新模块使用 ORM,老模块仍有字符串拼接 SQL。配置暴露:测试配置、默认口令、调试开关混入生产分支。日志与审计不足:关键操作没有记录操作者、对象、结果,事件复盘困难。审计时建议先不要急着跑工具扫一遍就结束。工具能帮忙发现一部分明显问题,但对业务权限、对象越权、流程缺陷的覆盖有限。更稳妥的方式是先梳理入口,再按风险类型分层排查。 审计前的准备代码审计前我通常会先整理三类信息:部署拓扑、路由入口、敏感功能。这样后续验证会更有边界,不容易误伤业务。 部署拓扑:Nginx 转发规则、Tomcat context、静态目录、上传目录、定时任务。路由入口:Controller、Filter、Interceptor、Servlet、JSP 直连页面。敏感功能:登录、用户管理、角色权限、文件上传下载、导入导出、报表查询、系统配置。可以先用简单命令对代码结构做一个粗略索引: find . -type f \( -name "*.java" -o -name "*.jsp" -o -name "*.xml" -o -name "*.properties" -o -name "*.yml" \) | wc -l grep -RIn "@RequestMapping\|@GetMapping\|@PostMapping" src/ | head -n 50 grep -RIn "upload\|download\|export\|import\|FileInputStream\|FileOutputStream" src/如果是 Spring Boot 项目,可以重点关注以下位置: 安全配置:SecurityConfig、WebMvcConfigurer、Interceptor 注册逻辑。统一异常:是否吞掉异常,是否把堆栈直接返回前端。配置文件:application-prod.yml、bootstrap.yml、logback 配置。依赖版本:pom.xml / build.gradle 中是否存在长期未升级组件。技术分析:几个高频风险点1. 权限校验分散导致的接口越权很多老系统会在页面层控制菜单显示,但后端接口本身没有强制鉴权。审计时不能只看登录拦截器是否存在,还要看“登录态”和“操作权限”是否分开校验。 一个比较危险的模式是只判断 session 中是否存在用户: if (session.getAttribute("user") == null) { return "redirect:/login"; }这种做法只能说明用户已登录,不能说明用户有权限访问某个接口。更合理的方式是让权限校验集中在 Filter、Interceptor 或安全框架中,并且权限点绑定到后端路由,而不是只绑定前端菜单。 排查时可以搜索: grep -RIn "getAttribute(\"user\"\)\|hasPermission\|checkPermission\|@PreAuthorize\|@RequiresPermissions" src/如果同一个项目里既有 @PreAuthorize,又有手写 session 判断,还存在完全没有注解的 Controller,就要重点抽样验证。验证时应在授权测试环境中使用不同角色账号对比访问结果,避免对生产环境做破坏性操作。 2. 文件上传与可访问目录叠加上传漏洞的风险不一定来自“上传后执行”,也可能来自敏感文件落地、覆盖文件、路径穿越、恶意内容被二次解析等。审计重点包括: 是否只依赖文件名后缀判断。是否使用用户传入的原始文件名直接落盘。上传目录是否在 Web 根目录下。下载接口是否存在任意文件读取风险。是否对文件大小、类型、数量做限制。常见的不安全写法如下: String fileName = multipartFile.getOriginalFilename(); File dest = new File(uploadPath + "/" + fileName); multipartFile.transferTo(dest);至少应做这些处理:生成服务端文件名、白名单扩展名、限制大小、校验真实文件类型、目录规范化、上传目录禁止执行、下载时按文件 ID 映射真实路径。 Path baseDir = Paths.get(uploadPath).toAbsolutePath().normalize(); String safeName = UUID.randomUUID().toString() + ".pdf"; Path target = baseDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new IllegalArgumentException("invalid path"); }Nginx 层也建议明确限制上传目录的执行能力,不要让上传目录被脚本引擎处理。示例仅表达思路,具体规则要结合实际路径调整: location /uploads/ { alias /data/app/uploads/; autoindex off; default_type application/octet-stream; add_header X-Content-Type-Options nosniff; }3. SQL 拼接与动态查询老系统里最常见的是“新代码用 MyBatis 参数绑定,老代码直接拼接条件”。尤其是报表、导出、综合查询页面,字段多、条件多,很容易出现拼接 SQL。 String sql = "select * from orders where 1=1"; if (StringUtils.isNotBlank(keyword)) { sql += " and customer_name like '%" + keyword + "%'"; }审计时可以搜索以下关键词: grep -RIn "Statement\|createQuery\|createNativeQuery\|\$\{\|order by \|like '%" src/需要注意,MyBatis 中 ${} 不等于 #{}。前者是字符串替换,后者是参数绑定。对于排序字段这类无法直接绑定的场景,应使用白名单映射,而不是把用户输入直接拼进 SQL。 Map<String, String> allowSort = new HashMap<>(); allowSort.put("time", "create_time"); allowSort.put("amount", "total_amount"); String sortColumn = allowSort.getOrDefault(req.getSort(), "create_time");4. 反序列化与表达式注入的边界排查Java 项目如果引入过老版本组件,反序列化和表达式注入需要单独看。这里不建议在业务环境做危险 payload 验证,更适合通过依赖分析、调用链确认、受控单元测试来判断风险。 可以先看依赖树: mvn dependency:tree | egrep "commons-collections|fastjson|jackson-databind|xstream|shiro|spring-expression"关注点包括: 是否存在历史高危版本组件。是否有对外暴露的反序列化入口,例如直接接收 ObjectInputStream、XMLDecoder。是否启用了危险的类型自动识别能力。表达式是否包含用户可控输入。判断这类风险时,不要只凭“存在某个依赖”就下结论。更准确的结论应包含:组件版本、是否可达、入口在哪里、用户输入能否控制关键参数、现有防护是否生效。 5. 配置与日志问题配置泄露和日志缺失经常被低估。比如测试环境回调地址、默认管理口令、对象存储密钥、数据库连接串被提交到仓库。审计时可以对配置文件做一次敏感信息扫描: grep -RIn "password\|passwd\|secret\|accessKey\|token\|jdbc:\|redis" . \ --exclude-dir=target --exclude-dir=.git日志方面,建议至少覆盖这些事件: 登录成功、失败、锁定、退出。新增、删除、修改用户与角色。导入、导出、下载敏感数据。系统配置变更。权限拒绝与异常访问。日志要避免记录明文密码、完整 token、身份证号等敏感数据。更推荐记录脱敏后的对象标识、操作者 ID、来源 IP、请求 ID、结果状态,便于后续排查。 关键步骤:从发现到修复闭环实际项目里,审计报告如果只列“存在漏洞”,研发往往难以处理。更有效的方式是把每个问题写成可复现、可定位、可修复、可复测的闭环。 阶段重点产出资产梳理确认代码分支、部署环境、入口接口接口清单、敏感功能列表静态审计定位高风险函数、配置、依赖疑似风险点与代码位置受控验证在授权测试环境验证影响复现步骤、请求样例、影响范围修复建议给出最小改动与长期方案修复 PR 或整改说明复测确认确认漏洞不可复现,避免引入回归复测记录、回归用例一个比较实用的问题记录模板如下: 问题名称:接口权限校验缺失 影响模块:/admin/user/export 代码位置:UserController.java:128 触发条件:普通登录用户可直接请求导出接口 影响说明:可能导致非授权用户导出用户列表 验证环境:测试环境,账号 role=test_user 修复建议:后端增加权限点校验,导出操作纳入审计日志 复测标准:无权限用户访问返回 403,并记录权限拒绝日志注意点不要在生产环境做破坏性验证,尤其是文件写入、批量导出、压力型请求。验证权限问题时,应使用授权测试账号,避免使用真实高权限账号做无关操作。报告里尽量避免夸大影响,能确认到哪里就写到哪里。修复建议要区分短期止血和长期治理,例如先加权限校验,再统一权限框架。依赖漏洞不要只看 CVE 列表,要确认组件是否可达、功能是否启用。涉及敏感数据的截图、日志、请求包应脱敏后再流转。总结内网 Web 资产的安全问题往往不是单个“高深漏洞”,而是长期迭代中留下的工程缺口。一次有效的代码审计,核心不是堆工具结果,而是围绕入口、权限、数据、文件、配置、日志建立清晰判断链。 我的经验是,最值得长期投入的是三件事:权限校验集中化、文件与数据访问最小化、关键操作可审计。把这些基础能力做好,很多常见风险会自然下降。审计报告也应尽量服务于修复闭环,让研发能按代码位置和复测标准逐项处理,这比单纯列风险更有价值。 代码审计、调用链与关键函数定位示意 背景 景很 很多 多内 内网 背景很 景很多 很多内 多内网 背景很多 景很多内 很多内网 系统 统上 上线 线时 时间 间早 系统上 统上线 上线时 线时间 时间早 系统上线 统上线时 上线时间 线时间早 业务 务依 依赖 赖重 业务依 务依赖 依赖重 业务依赖 务依赖重 平时 时很 很少 少有 有人 人系 统性 性审 审计 平时很 时很少 很少有 少有人 有人系 人系统 系统性 统性审 性审计 平时很少 时很少有 很少有人 少有人系 有人系统 人系统性 系统性审 统性审计 真正 正出 出问 问题 题时 真正出 正出问 出问题 问题时 真正出问 正出问题 出问题时 常见 见情 情况 况不 不是 是单 单点 点漏 漏洞 洞特 特别 别复 复杂 常见情 见情况 情况不 况不是 不是单 是单点 单点漏 点漏洞 漏洞特 洞特别 特别复 别复杂 常见情况 见情况不 情况不是 况不是单 不是单点 是单点漏 单点漏洞 点漏洞特 漏洞特别 洞特别复 特别复杂 而是 是几 几个 个普 普通 通风 风险 险叠 叠加 而是几 是几个 几个普 个普通 普通风 通风险 风险叠 险叠加 而是几个 是几个普 几个普通 个普通风 普通风险 通风险叠 风险叠加 老旧 旧框 框架 老旧框 旧框架 老旧框架 调试 试接 接口 口遗 遗留 调试接 试接口 接口遗 口遗留 调试接口 试接口遗 接口遗留 上传 传目 目录 录可 可访 访问 上传目 传目录 目录可 录可访 可访问 上传目录 传目录可 目录可访 录可访问 权限 限校 校验 验分 分散 权限校 限校验 校验分 验分散 权限校验 限校验分 校验分散 日志 志缺 缺失 日志缺 志缺失 日志缺失 本文 文整 整理 理一 一次 次偏 偏防 防守 守视 视角 角的 的代 代码 码审 计与 与风 险收 收敛 敛方 方法 本文整 文整理 整理一 理一次 一次偏 次偏防 偏防守 防守视 守视角 视角的 角的代 的代码 代码审 码审计 审计与 计与风 与风险 风险收 险收敛 收敛方 敛方法 本文整理 文整理一 整理一次 理一次偏 一次偏防 次偏防守 偏防守视 防守视角 守视角的 视角的代 角的代码 的代码审 代码审计 码审计与 审计与风 计与风险 与风险收 风险收敛 险收敛方 收敛方法 重点 点放 放在 在如 如何 何发 发现 现问 重点放 点放在 放在如 在如何 如何发 何发现 发现问 现问题 重点放在 点放在如 放在如何 在如何发 如何发现 何发现问 发现问题 验证 证风 险边 边界 验证风 证风险 风险边 险边界 验证风险 证风险边 风险边界 推动 动修 修复 推动修 动修复 推动修复 不涉 涉及 及未 未授 授权 权攻 攻击 击或 或恶 恶意 意利 利用 不涉及 涉及未 及未授 未授权 授权攻 权攻击 攻击或 击或恶 或恶意 恶意利 意利用 不涉及未 涉及未授 及未授权 未授权攻 授权攻击 权攻击或 攻击或恶 击或恶意 或恶意利 恶意利用 场景 景假 假设 设是 是企 企业 业内 内部 部自 自研 场景假 景假设 假设是 设是企 是企业 企业内 业内部 内部自 部自研 场景假设 景假设是 假设是企 设是企业 是企业内 企业内部 业内部自 内部自研 技术 术栈 栈为 技术栈 术栈为 技术栈为 混合 前端 端有 有少 少量 量老 前端有 端有少 有少量 少量老 前端有少 端有少量 有少量老 部署 署在 部署在 环境 目标 标不 目标不 标不是 目标不是 打穿 穿系 打穿系 穿系统 打穿系统 是通 通过 过代 计和 和受 受控 控验 而是通 是通过 通过代 过代码 审计和 计和受 和受控 受控验 控验证 而是通过 是通过代 通过代码 过代码审 码审计和 审计和受 计和受控 和受控验 受控验证 把可 可被 被利 用的 的缺 缺陷 陷收 敛到 到可 可修 把可被 可被利 被利用 利用的 用的缺 的缺陷 缺陷收 陷收敛 收敛到 敛到可 到可修 可修复 把可被利 可被利用 被利用的 利用的缺 用的缺陷 的缺陷收 缺陷收敛 陷收敛到 收敛到可 敛到可修 到可修复 可复 复测 测的 的清 清单 可复测 复测的 测的清 的清单 可复测的 复测的清 测的清单 题与 与场 景这 这类 类系 统常 见的 的问 题并 并不 不新 问题与 题与场 与场景 场景这 景这类 这类系 类系统 系统常 统常见 常见的 见的问 的问题 问题并 题并不 并不新 问题与场 题与场景 与场景这 场景这类 景这类系 这类系统 类系统常 系统常见 统常见的 常见的问 见的问题 的问题并 问题并不 题并不新 但因 因为 为业 务迭 迭代 代多 多年 但因为 因为业 为业务 业务迭
  9. 背景文件读取类漏洞在 Java Web 项目里并不稀奇,但实际排查时经常被低估。很多业务代码本意只是提供“下载日志”“导出附件”“预览模板”等功能,参数看起来也只是一个文件名,一旦路径拼接和权限校验处理不严,就可能变成任意文件读取。 这篇文章整理一次相对典型的代码审计和复盘过程。内容只讨论本地测试环境中的漏洞成因、验证方式和修复思路,不涉及真实目标攻击,也不提供越权利用指导。重点放在:如何在代码审计中识别风险点,如何构造安全的复现环境,以及如何把问题修干净。 场景描述某个内部 Java Web 系统提供附件下载接口,接口大致功能如下: 用户传入文件名参数。服务端从固定目录读取文件。以流的形式返回给浏览器下载。开发侧认为文件都存放在固定目录下,因此没有额外做权限检查。问题出在“固定目录 + 用户输入”这种组合上,如果直接字符串拼接路径,攻击者可能通过路径穿越读取目录外文件。 问题代码示例下面是经过简化的示例代码,便于说明问题。实际项目里类似逻辑可能散落在 Controller、Service 或工具类中。 String baseDir = "/data/app/uploads/"; String fileName = request.getParameter("file"); File file = new File(baseDir + fileName); if (!file.exists()) { response.setStatus(404); return; } try (InputStream in = new FileInputStream(file); OutputStream out = response.getOutputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } }这段代码的问题比较直接: 用户输入直接参与文件路径拼接。没有做路径规范化处理。没有校验最终文件是否仍位于允许目录下。接口层缺少业务权限判断,例如文件是否属于当前用户或当前租户。漏洞成因分析文件读取漏洞的核心并不只是“出现了 ../”,而是服务端没有把“用户可控文件标识”和“真实文件系统路径”隔离开。常见成因有几类。 1. 直接拼接路径这是最常见的问题。开发者把用户传入的文件名直接拼到基础目录后面,认为基础目录已经限制了访问范围。但文件系统会解析路径中的相对目录符号,最终可能跳出预期目录。 File file = new File(baseDir + userInput);即使对参数做了简单黑名单过滤,例如替换 ../,也容易被编码、不同分隔符、重复编码或平台差异影响。单纯靠字符串替换不是稳定的安全边界。 2. 只判断文件是否存在不少代码只检查 exists() 或 isFile(),但这些判断只能说明路径对应的文件存在,不能说明这个文件是否应该被当前用户访问。 if (file.exists() && file.isFile()) { // 返回文件 }安全判断应该包含两个层面:路径边界校验和业务授权校验。前者解决“能不能出目录”,后者解决“是不是你的文件”。 3. 使用原始文件名作为下载标识如果数据库中只保存原始文件名,下载接口又允许客户端直接传 fileName,会带来两个问题: 文件名可预测,容易枚举。文件名和路径边界耦合,容易出现路径穿越。更稳妥的做法是使用文件 ID 或随机存储名作为引用,由服务端根据数据库记录映射到真实路径。 本地复现思路复现建议只在本机或授权测试环境完成。可以用一个临时目录模拟上传目录,不需要读取系统敏感文件。下面以 Linux/macOS 环境为例,准备目录结构: mkdir -p /tmp/demo-upload mkdir -p /tmp/demo-secret echo "normal attachment" > /tmp/demo-upload/report.txt echo "should not be downloadable" > /tmp/demo-secret/internal.txt假设下载接口的 baseDir 指向 /tmp/demo-upload/,正常访问 report.txt 应该成功,而 /tmp/demo-secret/internal.txt 不应该被读取。测试时只需要验证接口是否可能越过 /tmp/demo-upload 访问上级目录中的测试文件即可。 如果漏洞存在,服务端日志里通常能看到最终被打开的路径并不在预期目录下。建议在本地调试时打印 canonical path,便于观察路径解析结果。 System.out.println("request fileName = " + fileName); System.out.println("absolute path = " + file.getAbsolutePath()); System.out.println("canonical path = " + file.getCanonicalPath());这里重点关注 canonical path,因为它会解析相对路径、符号链接等情况,比 getAbsolutePath 更接近真实访问路径。 安全修复方案修复文件读取漏洞时,不建议只做参数黑名单。更可靠的方案是“服务端映射 + 路径规范化 + 目录边界校验 + 业务授权”。 1. 使用文件 ID 映射真实路径下载接口尽量不要接受任意文件名,而是接受业务文件 ID。服务端根据 ID 查询数据库,确认文件归属和存储路径。 GET /download?id=12345数据库记录中建议至少包含: 文件 ID。上传用户或所属业务对象。存储目录或对象存储 key。原始文件名,仅用于展示和下载头。文件大小、哈希、创建时间等审计字段。这样客户端不能自由决定服务端读取哪个路径,风险会低很多。 2. 规范化路径并校验目录边界如果确实需要根据相对路径读取文件,必须在读取前做规范化,并确认最终路径仍在允许目录内。 Path basePath = Paths.get("/data/app/uploads").toRealPath(); String fileName = request.getParameter("file"); Path targetPath = basePath.resolve(fileName).normalize(); if (!targetPath.startsWith(basePath)) { response.setStatus(403); return; } if (!Files.isRegularFile(targetPath)) { response.setStatus(404); return; } try (InputStream in = Files.newInputStream(targetPath); OutputStream out = response.getOutputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } }这里有几个细节: basePath 使用 toRealPath() 获取真实路径。targetPath 使用 resolve() 和 normalize() 处理相对路径。通过 startsWith(basePath) 判断是否仍在允许目录内。读取前使用 Files.isRegularFile(),避免目录、设备文件等异常对象。3. 注意符号链接问题符号链接是文件读取修复中容易遗漏的点。如果上传目录内存在指向外部目录的软链接,单纯 normalize() 不一定能覆盖所有情况。更严格的做法是在最终读取前对目标文件调用 toRealPath(),再判断边界。 Path basePath = Paths.get("/data/app/uploads").toRealPath(); Path targetPath = basePath.resolve(fileName).normalize(); if (!Files.exists(targetPath)) { response.setStatus(404); return; } Path realTargetPath = targetPath.toRealPath(); if (!realTargetPath.startsWith(basePath)) { response.setStatus(403); return; }如果业务不需要软链接,部署层面也建议禁止在上传目录中创建软链接,或者在文件处理流程中显式拒绝。 4. 增加业务权限校验路径边界校验只能证明文件在上传目录里,不能证明当前用户有权下载。多租户系统、工单系统、网盘类系统尤其要注意横向越权。 推荐的校验流程: 根据文件 ID 查询文件记录。判断文件是否存在且状态正常。判断当前用户是否属于该文件的拥有者、协作者或有对应业务对象权限。通过权限校验后,再读取对应存储路径。Attachment attachment = attachmentDao.findById(fileId); if (attachment == null) { response.setStatus(404); return; } if (!permissionService.canDownload(currentUser, attachment)) { response.setStatus(403); return; }审计时的关注点做代码审计时,可以优先搜索以下关键词和调用点: new File( FileInputStream Files.newInputStream Files.readAllBytes getResourceAsStream ResourceUtils.getFile response.getOutputStream Content-Disposition同时关注参数名: file fileName path download template log report attach url key一些高风险业务接口包括: 附件下载、图片预览、报表导出。日志查看、日志下载。模板下载、配置文件预览。插件包、升级包、备份文件下载。临时文件读取接口。审计时不要只看 Controller。很多项目会在工具类里封装 downloadFile、writeFileToResponse、preview 等方法,多个接口复用同一段危险逻辑。 测试用例建议修复后建议补充单元测试或集成测试,覆盖正常路径、越界路径、编码路径、软链接等场景。示例用例可以包括: 正常下载上传目录中的文件。访问不存在文件时返回 404。访问目录时拒绝。相对路径跳出基础目录时返回 403。软链接指向外部目录时拒绝。无权限用户访问他人文件时返回 403。简单的 JUnit 思路如下: @Test void shouldRejectPathTraversal() throws Exception { Path base = Files.createTempDirectory("upload"); Path outside = Files.createTempFile("outside", ".txt"); Files.writeString(outside, "outside content"); String input = "../" + outside.getFileName(); boolean allowed = FilePathChecker.isSafe(base, input); assertFalse(allowed); }路径安全判断可以单独抽成工具方法,便于复用和测试。 public static boolean isSafe(Path baseDir, String userInput) throws IOException { Path basePath = baseDir.toRealPath(); Path targetPath = basePath.resolve(userInput).normalize(); if (!Files.exists(targetPath)) { return false; } Path realTargetPath = targetPath.toRealPath(); return realTargetPath.startsWith(basePath) && Files.isRegularFile(realTargetPath); }日志与监控文件读取类问题修复后,建议增加必要的审计日志。日志不需要记录完整文件内容,但应记录关键上下文,便于后续排查: 当前用户 ID。文件 ID 或业务对象 ID。请求来源 IP。规范化后的目标路径或存储 key。拒绝原因,例如越界、无权限、不存在。对明显异常的路径访问可以做告警,例如参数中出现连续的路径跳转符号、异常编码、访问大量不存在文件等。但监控只能作为辅助,不能替代代码层面的边界校验。 容易踩的坑只过滤 ../,没有处理 Windows 分隔符或编码差异。只判断 absolute path,没有判断 canonical/real path。修了下载接口,遗漏了预览接口。上传目录允许创建软链接,但读取时没有处理。只做路径校验,没有做文件归属权限校验。下载头直接使用用户输入文件名,导致响应头异常或兼容性问题。下载文件名建议从数据库中的原始文件名读取,并进行安全处理,避免换行符等特殊字符影响响应头。 String safeName = originalName.replaceAll("[\\r\\n]", "_"); response.setHeader("Content-Disposition", "attachment; filename=\"" + safeName + "\"");总结文件读取漏洞看起来简单,但在真实项目里经常和权限设计、存储设计、历史接口复用混在一起。比较稳妥的治理方式不是临时加几个字符串过滤,而是把文件访问抽象成统一能力:客户端只传文件 ID,服务端负责权限判断、路径映射和边界校验。 如果只能先做短期修复,至少要做到三点:规范化路径、校验最终真实路径仍在允许目录内、确认当前用户有业务访问权限。后续再逐步把直接传路径的接口改造成文件 ID 访问模式,减少同类问题反复出现。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件读 读取 取类 类漏 漏洞 洞在 背景文 景文件 文件读 件读取 读取类 取类漏 类漏洞 漏洞在 背景文件 景文件读 文件读取 件读取类 读取类漏 取类漏洞 类漏洞在 项目 目里 里并 并不 不稀 稀奇 项目里 目里并 里并不 并不稀 不稀奇 项目里并 目里并不 里并不稀 并不稀奇 但实 实际 际排 排查 查时 时经 经常 常被 被低 低估 但实际 实际排 际排查 排查时 查时经 时经常 经常被 常被低 被低估 但实际排 实际排查 际排查时 排查时经 查时经常 时经常被 经常被低 常被低估 很多 多业 业务 务代 代码 码本 本意 意只 只是 是提 提供 很多业 多业务 业务代 务代码 代码本 码本意 本意只 意只是 只是提 是提供 很多业务 多业务代 业务代码 务代码本 代码本意 码本意只 本意只是 意只是提 只是提供 下载 载日 日志 下载日 载日志 下载日志 导出 出附 附件 导出附 出附件 导出附件 预览 览模 模板 预览模 览模板 预览模板 等功 功能 等功能 参数 数看 看起 起来 来也 也只 是一 一个 个文 件名 参数看 数看起 看起来 起来也 来也只 也只是 只是一 是一个 一个文 个文件 文件名 参数看起 数看起来 看起来也 起来也只 来也只是 也只是一 只是一个 是一个文 一个文件 个文件名 一旦 旦路 路径 径拼 拼接 接和 和权 权限 限校 校验 验处 处理 理不 不严 一旦路 旦路径 路径拼 径拼接 拼接和 接和权 和权限 权限校 限校验 校验处 验处理 处理不 理不严 一旦路径 旦路径拼 路径拼接 径拼接和 拼接和权 接和权限 和权限校 权限校验 限校验处 校验处理 验处理不 处理不严 就可 可能 能变 变成 成任 任意 意文 就可能 可能变 能变成 变成任 成任意 任意文 意文件 就可能变 可能变成 能变成任 变成任意 成任意文 任意文件 意文件读 这篇 篇文 文章 章整 整理 理一 一次 次相 相对 对典 典型 型的 的代 码审 审计 计和 和复 复盘 盘过 过程 这篇文 篇文章 文章整 章整理 整理一 理一次 一次相 次相对 相对典 对典型 典型的 型的代 的代码 代码审 码审计 审计和 计和复 和复盘 复盘过 盘过程 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次相 一次相对 次相对典 相对典型 对典型的 典型的代 型的代码 的代码审 代码审计 码审计和 审计和复 计和复盘 和复盘过 复盘过程 内容 容只 只讨 讨论 论本 本地 地测 测试 试环 环境 境中 中的 的漏 洞成 成因 内容只 容只讨 只讨论 讨论本 论本地 本地测 地测试 测试环 试环境 环境中 境中的 中的漏 的漏洞 漏洞成 洞成因 内容只讨 容只讨论 只讨论本 讨论本地 论本地测 本地测试 地测试环 测试环境 试环境中 环境中的 境中的漏 中的漏洞 的漏洞成 漏洞成因 验证 证方 方式 式和 和修 修复 复思 思路 验证方 证方式 方式和 式和修 和修复 修复思 复思路 验证方式 证方式和 方式和修 式和修复 和修复思 修复思路 不涉 涉及 及真 真实 实目 目标 标攻 攻击 不涉及 涉及真 及真实 真实目 实目标 目标攻 标攻击 不涉及真 涉及真实 及真实目 真实目标 实目标攻 目标攻击 也不 不提 供越 越权 权利 利用 用指 指导 也不提 不提供 提供越 供越权 越权利 权利用 利用指 用指导 也不提供 不提供越 提供越权 供越权利 越权利用 权利用指 利用指导 重点 点放 放在 重点放 点放在 重点放在 如何 何在 在代 计中 中识 识别 别风 风险 险点 如何在 何在代 在代码 审计中 计中识 中识别 识别风 别风险 风险点 如何在代 何在代码 在代码审 码审计中 审计中识 计中识别 中识别风 识别风险 别风险点 何构 构造 造安 安全 全的 的复 复现 现环 如何构 何构造 构造安 造安全 安全的 全的复 的复现 复现环 现环境 如何构造 何构造安 构造安全 造安全的 安全的复 全的复现 的复现环 复现环境 以及 及如 何把 把问 问题 题修 修干 干净 以及如 及如何 如何把 何把问 把问题 问题修 题修干 修干净 以及如何 及如何把 如何把问 何把问题 把问题修 问题修干 题修干净 场景 景描 描述 述某 某个 个内 内部 场景描 景描述 描述某 述某个 某个内 个内部 场景描述 景描述某 描述某个 述某个内 某个内部 系统 统提 供附 件下 载接 接口 系统提 统提供 提供附 供附件 附件下 件下载 下载接 载接口 系统提供 统提供附 提供附件 供附件下 附件下载 件下载接 下载接口 口大 大致 致功 能如
  10. SSRF(Server-Side Request Forgery,服务端请求伪造)在实际项目中并不一定表现为一个明显的“请求外部 URL”接口。图片抓取、Webhook 调试、文件导入、URL 预览、第三方回调检测等功能,都可能让服务器代替用户发起网络请求。审计这类问题时,单纯搜索 requests.get 或 HttpClient 往往不够,更重要的是沿着“用户可控输入—URL 解析—网络连接—响应处理”的完整链路确认风险边界。 一、问题背景与典型场景 SSRF 的核心不是“服务器访问了一个 URL”,而是攻击者能够影响服务器访问的目标、协议、端口或请求内容,并借助服务器所在网络环境获得普通客户端无法获得的访问能力。 图片或网页预览:后端接收 URL,下载内容后返回给前端。 Webhook 测试:用户输入回调地址,服务端向该地址发送测试请求。 文档解析:服务端根据文档中的外部资源地址加载图片、字体或样式。 云资源集成:后端根据用户提交的地址访问对象存储、内部 API 或代理服务。 健康检查功能:管理员输入地址,系统主动探测服务是否可用。 这些功能本身未必存在问题,真正需要关注的是:目标地址是否完全可控,校验发生在 DNS 解析前还是后,是否允许跟随重定向,以及请求是否携带了内部身份凭据。 二、审计时先画出完整数据流 建议不要从某一个危险函数开始,而是先建立数据流图。一个典型链路如下: HTTP 参数 / JSON 字段 ↓ URL 规范化与解析 ↓ 协议、主机、端口校验 ↓ DNS 解析 ↓ 建立 TCP / TLS 连接 ↓ 发送请求并处理重定向 ↓ 读取响应、保存文件或转发给用户 审计时可以分别回答以下问题: 输入来自哪里,是否经过了鉴权和权限校验? 校验的是原始字符串、解析后的主机名,还是最终连接的 IP 地址? 是否允许 http 以外的协议,例如 file、gopher 或自定义协议? DNS 解析是否可能返回多个地址,程序是否逐个检查? 请求是否自动跟随 30x 重定向,重定向后的地址是否重新校验? 是否存在代理配置、IPv6、Unix Socket 或其他非预期的网络路径? 响应内容是否会被当作 HTML、XML、图片或压缩包继续解析? 三、最容易被忽略的校验绕过点 1. 只做字符串黑名单 例如仅判断 URL 中是否包含 127.0.0.1、localhost 或某个内部域名。这种做法很脆弱,因为同一个地址可能存在不同表示形式,IPv4、IPv6、十进制整数、短格式地址以及大小写和尾点等写法,都可能导致字符串判断与实际连接目标不一致。 正确思路是先使用标准 URL 解析器拆分协议、主机、端口和路径,再对规范化后的主机进行解析,并对解析结果执行 IP 分类判断。校验对象应该是“最终要建立连接的 IP”,而不是未经解析的原始字符串。 2. DNS 解析与连接之间存在时间窗口 有些实现先调用 DNS 解析判断地址是否安全,之后又把原始域名交给 HTTP 客户端建立连接。两次解析的结果可能不同,尤其是在攻击者可控 DNS、短 TTL 或代理环境下,检查结果和实际连接目标可能不一致。 更稳妥的做法是: 由应用统一完成解析并取得地址列表。 对所有解析结果进行安全判断,而不是只检查第一个地址。 建立连接时固定使用已校验的地址,同时保留正确的 Host 和 TLS Server Name。 在网络层再增加出口控制,避免应用层校验失效后直接访问敏感网段。 3. 重定向导致校验失效 初始地址可能是公开站点,但服务器返回重定向后,目标变成了内网地址。如果 HTTP 客户端默认跟随重定向,初始校验就失去了意义。 推荐默认关闭自动重定向。如果业务必须支持重定向,应当在每一次跳转前重新执行协议、主机、端口和解析结果校验,并限制最大跳转次数。 四、一个防御性 URL 校验示例 下面示例展示的是审计和防护思路,不负责发起请求。实际项目中还需要结合网络出口策略和 HTTP 客户端的连接控制。 from ipaddress import ip_address from urllib.parse import urlparse import socket ALLOWED_SCHEMES = {"http", "https"} ALLOWED_PORTS = {80, 443} def resolve_all(hostname, port): result = socket.getaddrinfo( hostname, port, type=socket.SOCK_STREAM, ) return {item[4][0] for item in result} def is_public_ip(value): ip = ip_address(value) return not ( ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified ) def validate_url(raw_url): parsed = urlparse(raw_url) if parsed.scheme not in ALLOWED_SCHEMES: raise ValueError("unsupported scheme") if not parsed.hostname: raise ValueError("missing hostname") port = parsed.port or (443 if parsed.scheme == "https" else 80) if port not in ALLOWED_PORTS: raise ValueError("unsupported port") addresses = resolve_all(parsed.hostname, port) if not addresses: raise ValueError("hostname has no address") if not all(is_public_ip(addr) for addr in addresses): raise ValueError("destination is not public") return { "scheme": parsed.scheme, "hostname": parsed.hostname, "port": port, "path": parsed.path or "/", } 这个示例有几个值得注意的点:只允许业务需要的协议;端口采用白名单而不是简单排除少数端口;对 DNS 返回的全部地址进行检查;同时拒绝回环、私有、链路本地、组播、保留和未指定地址。 不过,应用层代码仍然可能受到解析与连接之间的竞态影响。因此生产环境不应只依赖这段逻辑,还应通过防火墙、容器网络策略、云安全组或 egress proxy 限制服务端出站访问。 五、HTTP 客户端配置中的关键防线 审计代码时,除了查找 URL 参数,还要检查 HTTP 客户端的默认行为。以下配置项经常决定漏洞是否可以被稳定利用: 配置项风险建议 自动跟随重定向初始地址通过校验后跳转到受限网络默认关闭;每次跳转重新校验 连接超时内网不可达地址造成线程、协程或连接池耗尽设置较短的连接和读取超时 响应体上限下载超大文件导致内存或磁盘资源耗尽限制响应大小并流式读取 代理继承环境变量或全局代理改变实际网络路径明确配置代理,不要无意继承 协议处理器非 HTTP 协议带来本地文件或其他资源访问能力显式限制为业务所需协议 请求头转发把用户请求中的 Cookie、Authorization 等凭据转发给目标建立明确的请求头白名单 六、代码审计中的搜索与回溯方法 在大型代码库中,可以先进行关键词粗筛,再结合调用链回溯。不同语言的关键词不完全相同,但思路一致。 输入来源:url、uri、callback、webhook、image、preview、proxy。 网络请求:requests、urllib、HttpClient、RestTemplate、WebClient、axios。 底层连接:socket、connect、getaddrinfo、net.Dial。 重定向和代理:followRedirects、redirect、proxy、NO_PROXY。 找到请求函数后,不要立即下结论。需要继续向上确认输入是否可控,向下确认是否存在 DNS 校验、重定向限制、超时和响应体限制。对封装较深的项目,还应关注统一 HTTP SDK、网关代理和异步任务消费者,因为真正的请求可能不在控制器或路由函数中。 七、在隔离环境中进行安全验证 漏洞验证应尽量使用自建的测试服务,不要扫描或请求真实内网、云平台管理接口及第三方系统。可以在本地或专用测试网络中启动两个服务:一个作为公开入口,另一个模拟受限服务,并在日志中记录访问来源和请求内容。 # 启动一个仅用于测试的本地 HTTP 服务 python3 -m http.server 18080 --bind 127.0.0.1 # 调用待测应用的 URL 预览接口,具体路径按测试项目调整 curl --max-time 5 \ -H 'Content-Type: application/json' \ -d '{"url":" \ 验证重点不是获取敏感信息,而是确认应用是否真的代替客户端发起了请求。可以通过测试服务日志、应用审计日志、出站防火墙日志和请求链路追踪共同确认。测试结束后应清理临时服务、测试数据和放行规则。 八、修复方案不能只停留在黑名单 较完整的防护通常分为四层: 业务层:尽量不要让用户直接提交任意 URL。对于图片、Webhook 等功能,使用域名或资源 ID 白名单。 应用层:限制协议、端口、跳转次数、响应大小和超时时间;对解析后的全部 IP 做分类检查。 网络层:服务默认禁止访问管理网段、容器网段、云平台链路本地地址和不必要的内部服务,仅放行明确的外部目的地。 监控层:记录目标域名、解析地址、端口、响应状态、耗时、重定向链和拒绝原因,建立异常访问告警。 如果业务确实需要访问外部资源,可以考虑引入专用出站代理。应用只向代理提交经过规范化的目标信息,由代理统一执行域名白名单、DNS 校验、IP 限制、内容大小限制和审计记录。这样比让每个业务模块自行实现一套校验逻辑更容易维护。 九、容易遗漏的延伸风险 响应内容二次解析:下载的 XML、SVG、HTML 或压缩包可能继续触发外部资源加载或路径穿越问题。 凭据泄露:统一 HTTP 客户端可能自动附加服务账号令牌、内部 Cookie 或代理认证信息。 资源耗尽:目标服务长时间不响应、持续返回大文件或返回大量重定向,都可能形成拒绝服务。 IPv6 双栈差异:只检查 IPv4 而忽略 IPv6,会造成策略不一致。 域名白名单过宽:允许整个组织域名后,子域接管、测试环境和遗留服务可能扩大访问范围。 异步任务绕过鉴权:前端接口做了限制,但后台任务队列消费时没有重新校验目标地址。 十、审计结论应如何记录 一份可复用的漏洞记录至少应包含以下内容: 受影响功能、接口和参数。 从输入到网络请求的完整调用链。 是否允许任意协议、端口、重定向和响应类型。 解析前校验与最终连接目标是否一致。 在隔离环境中的验证结果和日志证据。 业务影响、受影响网络范围以及是否可能泄露服务端凭据。 短期修复、长期架构改进和回归测试用例。 回归测试不应只测试一个被禁止的地址,还应覆盖重定向、多个 DNS 记录、IPv6、异常端口、超大响应、超时、非法协议和异步任务等场景。否则修复很容易只针对当前样例,而没有真正收紧请求能力。 总结 SSRF 审计的关键不在于记住若干黑名单地址,而在于明确“用户控制的 URL 最终连接到了哪里”。可靠的判断应覆盖 URL 解析、DNS 解析、实际连接、重定向和响应处理全过程,并由应用策略与网络出口策略共同兜底。 从工程角度看,最值得优先落实的措施是:限制业务需求范围、关闭或重新校验重定向、对所有解析地址进行检查、设置超时和响应上限、禁止转发敏感请求头,并在网络层实施默认拒绝的出站策略。这样即使某个业务模块的校验出现遗漏,也不会轻易演变成对内部服务和凭据的横向影响。 服务器、云资产与权限边界梳理示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 在实 实际 际项 项目 目中 中并 并不 不一 一定 定表 表现 现为 为一 一个 个明 明显 显的 在实际 实际项 际项目 项目中 目中并 中并不 并不一 不一定 一定表 定表现 表现为 现为一 为一个 一个明 个明显 明显的 在实际项 实际项目 际项目中 项目中并 目中并不 中并不一 并不一定 不一定表 一定表现 定表现为 表现为一 现为一个 为一个明 一个明显 个明显的 求外 外部 请求外 求外部 请求外部 接口 图片 片抓 抓取 图片抓 片抓取 图片抓取 调试 文件 件导 导入 文件导 件导入 文件导入 预览 第三 三方 方回 回调 调检 检测 测等 等功 功能 第三方 三方回 方回调 回调检 调检测 检测等 测等功 等功能 第三方回 三方回调 方回调检 回调检测 调检测等 检测等功 测等功能 都可 可能 能让 让服 务器 器代 代替 替用 用户 户发 发起 起网 网络 络请 都可能 可能让 能让服 让服务 服务器 务器代 器代替 代替用 替用户 用户发 户发起 发起网 起网络 网络请 络请求 都可能让 可能让服 能让服务 让服务器 服务器代 务器代替 器代替用 代替用户 替用户发 用户发起 户发起网 发起网络 起网络请 网络请求 审计 计这 这类 类问 问题 题时 审计这 计这类 这类问 类问题 问题时 审计这类 计这类问 这类问题 类问题时 单纯 纯搜 搜索 单纯搜 纯搜索 单纯搜索 往往 往不 不够 往往不 往不够 往往不够 更重 重要 要的 的是 是沿 沿着 更重要 重要的 要的是 的是沿 是沿着 更重要的 重要的是 要的是沿 的是沿着 户可 可控 控输 输入 用户可 户可控 可控输 控输入 用户可控 户可控输 可控输入 解析 络连 连接 网络连 络连接 网络连接 响应 应处 处理 响应处 应处理 响应处理 的完 完整 整链 链路 路确 确认 认风 风险 险边 边界 的完整 完整链 整链路 链路确 路确认 确认风 认风险 风险边 险边界 的完整链 完整链路 整链路确 链路确认 路确认风 确认风险 认风险边 风险边界 题背 背景 景与 与典 典型 型场 场景 问题背 题背景 背景与 景与典 与典型 典型场 型场景 问题背景 题背景与 背景与典 景与典型 与典型场 典型场景 的核 核心 心不 不是 的核心 核心不 心不是 的核心不 核心不是 器访 访问 问了 了一 务器访 器访问 访问了 问了一 了一个 服务器访 务器访问 器访问了 访问了一 问了一个 而是 是攻 攻击 击者 者能 能够 够影 影响 响服 问的 的目 目标 而是攻 是攻击 攻击者 击者能 者能够 能够影 够影响 影响服 响服务 访问的 问的目 的目标 而是攻击 是攻击者 攻击者能 击者能够 者能够影 能够影响 够影响服 影响服务 响服务器 器访问的 访问的目 问的目标 协议 端口 口或 或请 求内 内容 端口或 口或请 或请求 请求内 求内容 端口或请 口或请求 或请求内 请求内容 并借 借助 助服 器所 所在 在网 络环 环境 境获 获得 得普 普通 通客 客户 户端 端无 无法 法获 得的 的访 问能 能力 并借助 借助服 助服务 务器所 器所在 所在网 在网络 网络环 络环境 环境获 境获得 获得普 得普通 普通客 通客户 客户端 户端无 端无法 无法获 法获得 获得的 得的访 的访问 访问能 问能力 并借助服 借助服务 助服务器 服务器所 务器所在 器所在网 所在网络 在网络环 网络环境 络环境获 环境获得 境获得普 获得普通 得普通客 普通客户 通客户端 客户端无 户端无法 端无法获 无法获得 法获得的 获得的访 得的访问 的访问能 访问能力 片或 或网 网页 页预 图片或 片或网 或网页 网页预 页预览 图片或网 片或网页 或网页预 网页预览 后端 端接 接收 后端接 端接收 后端接收 下载 载内 容后 后返 返回 回给 给前 前端 下载内 载内容 内容后 容后返 后返回 返回给 回给前 给前端 下载内容 载内容后 内容后返 容后返回 后返回给 返回给前 回给前端 测试 户输 入回 调地 地址 用户输 户输入 输入回 入回调 回调地 调地址 用户输入 户输入回 输入回调 入回调地 回调地址 端向 向该 该地 址发 发送 送测 试请 务端向 端向该 向该地 该地址 地址发 址发送 发送测 送测试 测试请 试请求 服务端向 务端向该 端向该地 向该地址 该地址发 地址发送 址发送测 发送测试 送测试请 测试请求 文档 档解 文档解 档解析 文档解析 端根 根据
  11. 背景最近整理了一次 Web 服务器可疑文件告警的排查过程,场景并不复杂:业务侧反馈某台 PHP 应用服务器被安全设备提示存在疑似 WebShell 文件,但没有明确说明入口点。类似事件在日常应急里很常见,难点通常不在于“删掉一个可疑文件”,而是要判断它从哪里来、是否被执行过、是否还有持久化或横向痕迹。 这篇文章按一次相对完整的排查链路来写,重点放在防守视角的取证、日志关联、代码审计和加固建议上,不涉及攻击利用细节。 问题场景已知信息比较有限: 服务器为 Linux,运行 Nginx + PHP-FPM。业务目录存在近期新增的可疑 PHP 文件。安全设备提示文件具有 WebShell 特征。业务方不确定是否存在异常登录或数据泄露。这类场景建议先不要急着删除文件。直接删除会破坏时间戳、文件内容和权限等取证信息,也可能漏掉其他关联文件。更稳妥的做法是先隔离访问、保全证据,再做关联分析。 初始处置:先控风险,再保全如果业务允许,优先在网关、WAF 或 Nginx 层面临时限制该站点的外部访问,避免可疑文件继续被触发。对于生产环境,通常不建议立即关机,因为关机会丢失部分内存态和进程信息。可以先做以下保全: mkdir -p /root/incident-$(date +%F) cd /root/incident-$(date +%F) # 记录基本系统信息 date > basic.txt hostname >> basic.txt whoami >> basic.txt uname -a >> basic.txt # 记录进程、网络连接、登录情况 ps auxww > ps_auxww.txt ss -antup > ss_antup.txt last -a > last_login.txt lastb -a > lastb_login.txt 2>/dev/null w > w.txt # 记录计划任务 crontab -l > crontab_current_user.txt 2>/dev/null ls -lah /etc/cron* > cron_dirs.txt 2>/dev/null同时对可疑文件做只读复制,保留哈希: cp -a /var/www/html/path/suspect.php ./suspect.php.copy sha256sum /var/www/html/path/suspect.php ./suspect.php.copy > suspect_hash.txt stat /var/www/html/path/suspect.php > suspect_stat.txt注意不要在原文件上直接使用编辑器打开并保存,避免改变元数据。 技术分析一:确认可疑文件特征对 WebShell 的初步识别,一般从代码结构、敏感函数、混淆方式和输入来源几方面入手。防守分析中重点不是复现控制效果,而是确认它是否具备远程执行、文件操作、命令执行、信息回显等能力。 常见风险点包括: 动态执行:如 eval、assert、preg_replace 的历史危险用法、create_function 等。系统调用:如 system、exec、shell_exec、passthru、popen、proc_open。文件操作:如 file_put_contents、fwrite、unlink、copy、move_uploaded_file。输入来源:如 $_POST、$_GET、$_COOKIE、php://input。混淆痕迹:如 base64_decode、gzinflate、str_rot13、chr 拼接、大量不可读变量名。# 在业务目录内做敏感函数检索,仅用于排查 cd /var/www/html grep -RIn --exclude-dir=vendor --exclude-dir=node_modules \ -E "eval\s*\(|assert\s*\(|system\s*\(|exec\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(|base64_decode\s*\(|gzinflate\s*\(" . > /root/incident-grep.txt检索结果不能直接等同于木马。很多框架、依赖包或运维脚本也可能使用类似函数。因此要结合文件路径、修改时间、代码风格、提交记录和访问日志判断。比如一个位于 uploads 目录下的 PHP 文件,风险通常高于框架核心目录里的正常代码。 技术分析二:从时间线找入口入侵排查里,时间线非常关键。先看可疑文件的创建、修改时间,再回溯这一时间点前后的访问日志、错误日志和上传目录变化。 # 查看近期变更的 PHP 文件 find /var/www/html -type f -name "*.php" -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %p\n" | sort > recent_php_files.txt # 查看近期上传目录文件 find /var/www/html -type f -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %s %p\n" | sort > recent_all_files.txt如果文件系统支持,可以进一步查看 ctime。ctime 不是创建时间,而是 inode 元数据变化时间,但在排查中很有参考价值: stat /var/www/html/path/suspect.php随后关联 Nginx access log。假设可疑文件修改时间在 10:20 左右,可以提取前后半小时日志: grep "10/Oct/2026:10:" /var/log/nginx/access.log > access_10h.txt重点关注以下特征: 是否有上传接口在可疑时间点被访问。是否有管理后台、插件安装、模板编辑等高风险功能被调用。是否出现异常 User-Agent、非常规 HTTP 方法或大量 4xx/5xx。是否从同一 IP 连续访问上传接口和可疑 PHP 文件。是否存在对配置文件、备份文件、接口文档的探测请求。如果日志量较大,可以先按状态码、路径、IP 聚合: # 按 IP 统计访问量 awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head # 按 URL 聚合,适合先看异常路径 awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -50 # 查看 4xx/5xx 请求 awk '$9 ~ /^[45]/ {print $0}' /var/log/nginx/access.log | head -200技术分析三:排查上传与文件写入链路WebShell 落地常见原因包括上传校验不足、后台弱口令、组件漏洞、部署目录权限过大、历史测试脚本遗留等。排查时不建议只盯着一个可疑文件,而要把“文件如何写入”作为核心问题。 可以从以下几个方向检查: 上传接口是否只校验前端或 Content-Type,而没有服务端白名单。上传后的文件是否保存在 Web 可执行目录下。Nginx 是否错误地将 uploads、attachment、public 等目录交给 PHP 解析。后台是否存在文件管理、模板编辑、插件安装等功能。应用日志中是否记录了文件上传、用户登录、后台操作。一个比较稳妥的 Nginx 加固方式,是禁止上传目录内 PHP 解析: location ~* ^/(uploads|attachment|static)/.*\.php$ { return 403; }如果业务允许,更推荐让上传文件使用对象存储或独立静态域名,并确保该域名不具备脚本解析能力。即使上传校验出现疏漏,也能降低直接执行风险。 技术分析四:检查 PHP-FPM 与权限边界很多服务器被入侵后影响扩大,并不是单个漏洞本身多严重,而是运行权限过大。例如 PHP-FPM 使用高权限用户运行、业务目录整体可写、配置文件包含数据库明文密码且权限过宽。 建议检查 PHP-FPM 运行用户: ps aux | grep php-fpm | grep -v grep cat /etc/php-fpm.d/ 2>/dev/null | grep -E "^(user|group)\s*="再检查 Web 目录权限: find /var/www/html -type d -perm -0002 -ls > world_writable_dirs.txt find /var/www/html -type f -perm -0002 -ls > world_writable_files.txt find /var/www/html -type f -name "*.php" -perm -0200 -ls | head -100原则上,代码目录不应该被 Web 运行用户随意写入。确实需要写入的目录,如缓存、日志、上传目录,应尽量单独隔离,并禁止脚本执行。 技术分析五:判断是否存在后续动作确认 WebShell 后,还需要判断是否被访问过、是否执行过异常命令、是否留下持久化。可以从以下位置排查: Nginx access/error log:是否请求过可疑文件。PHP-FPM slow log/error log:是否有异常报错或慢请求。系统认证日志:是否有异常 SSH 登录、sudo 行为。计划任务:是否新增异常任务。启动项:systemd、rc.local、profile 等是否被修改。临时目录:/tmp、/var/tmp、/dev/shm 是否有异常文件。# 查找临时目录近期文件 find /tmp /var/tmp /dev/shm -type f -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %s %p\n" 2>/dev/null | sort # 查看系统登录与认证日志,路径按发行版不同可能有差异 grep -Ei "Accepted|Failed|sudo|session" /var/log/auth.log 2>/dev/null | tail -300 grep -Ei "Accepted|Failed|sudo|session" /var/log/secure 2>/dev/null | tail -300 # systemd 服务变更排查 find /etc/systemd/system /usr/lib/systemd/system -type f -mtime -30 -ls 2>/dev/null如果发现 Web 运行用户发起异常外连,也需要结合 ss 结果、进程启动时间、父子进程关系进一步确认。对于仍在运行的可疑进程,先记录命令行、环境变量、打开文件,再考虑处置: # 假设 PID 为 1234 cat /proc/1234/cmdline | tr '\0' ' ' ls -lah /proc/1234/cwd /proc/1234/exe ls -lah /proc/1234/fd | head cat /proc/1234/environ | tr '\0' '\n' | head代码审计关注点如果初步判断入口在业务代码,建议围绕可疑时间点访问过的接口做定向审计,而不是全站盲扫。以文件上传为例,审计重点包括: 扩展名校验是否使用服务端白名单,例如仅允许 jpg、png、pdf 等。是否校验真实文件内容,避免只看后缀或 MIME。上传后的文件名是否随机化,是否保留用户原始文件名。是否允许双后缀、大小写绕过、特殊字符路径。上传目录是否与代码目录混放。是否存在任意路径写入、目录穿越或解压缩覆盖问题。一个更安全的上传处理思路是:服务端白名单扩展名、重新生成文件名、固定存储目录、关闭脚本解析、记录上传审计日志。对于图片类文件,可使用图像库重新编码后再存储,减少夹带非预期内容的风险。 处置与恢复建议完成证据保全和影响面判断后,再做处置。常见动作包括: 删除或隔离确认恶意的文件,并记录哈希和路径。修复入口漏洞,例如上传校验、后台弱口令、组件漏洞。调整目录权限,禁止上传目录脚本执行。轮换数据库、后台、服务器账号等敏感凭据。检查同集群、同镜像、同代码仓库是否存在相同问题。补齐日志留存,避免后续无法追溯。如果无法确认攻击者是否获得系统权限,建议不要只在原机上“清理一下继续跑”。更稳妥的方式是基于可信镜像重建环境,回滚干净代码和数据,再将修复后的配置上线。 常见误区只删除 WebShell,不找入口。这样很容易复发。只看文件修改时间,不关联访问日志。时间线会断。把 grep 到敏感函数的文件都当木马。容易误判业务代码。让上传目录继续支持 PHP 解析。修复上传校验也不能替代执行隔离。忽略凭据轮换。配置文件、环境变量、日志中都可能暴露敏感信息。总结WebShell 告警的排查重点不是单点文件识别,而是围绕“落地、执行、扩展、持久化”建立证据链。实际工作中,可以按这条线推进:先隔离和保全,再确认文件特征,随后用时间线关联访问日志和业务日志,最后修复入口并加固权限边界。 长期看,减少这类事件的关键不只是上设备告警,而是把基础防线做好:上传目录不可执行、代码目录最小权限、后台强认证、组件及时更新、日志可追溯。很多入侵并不依赖复杂技巧,往往是几个基础配置同时失守后被放大。 网络请求、日志与边界流量分析示意 背景 景最 最近 近整 整理 理了 了一 一次 背景最 景最近 最近整 近整理 整理了 理了一 了一次 背景最近 景最近整 最近整理 近整理了 整理了一 理了一次 服务 务器 器可 可疑 疑文 文件 件告 告警 警的 的排 排查 查过 过程 服务器 务器可 器可疑 可疑文 疑文件 文件告 件告警 告警的 警的排 的排查 排查过 查过程 服务器可 务器可疑 器可疑文 可疑文件 疑文件告 文件告警 件告警的 告警的排 警的排查 的排查过 排查过程 场景 景并 并不 不复 复杂 场景并 景并不 并不复 不复杂 场景并不 景并不复 并不复杂 业务 务侧 侧反 反馈 馈某 某台 业务侧 务侧反 侧反馈 反馈某 馈某台 业务侧反 务侧反馈 侧反馈某 反馈某台 应用 用服 器被 被安 安全 全设 设备 备提 提示 示存 存在 在疑 疑似 应用服 用服务 务器被 器被安 被安全 安全设 全设备 设备提 备提示 提示存 示存在 存在疑 在疑似 应用服务 用服务器 服务器被 务器被安 器被安全 被安全设 安全设备 全设备提 设备提示 备提示存 提示存在 示存在疑 存在疑似 但没 没有 有明 明确 确说 说明 明入 入口 口点 但没有 没有明 有明确 明确说 确说明 说明入 明入口 入口点 但没有明 没有明确 有明确说 明确说明 确说明入 说明入口 明入口点 类似 似事 事件 件在 在日 日常 常应 应急 急里 里很 很常 常见 类似事 似事件 事件在 件在日 在日常 日常应 常应急 应急里 急里很 里很常 很常见 类似事件 似事件在 事件在日 件在日常 在日常应 日常应急 常应急里 应急里很 急里很常 里很常见 难点 点通 通常 常不 不在 在于 难点通 点通常 通常不 常不在 不在于 难点通常 点通常不 通常不在 常不在于 删掉 掉一 一个 个可 删掉一 掉一个 一个可 个可疑 删掉一个 掉一个可 一个可疑 个可疑文 而是 是要 要判 判断 断它 它从 从哪 哪里 里来 而是要 是要判 要判断 判断它 断它从 它从哪 从哪里 哪里来 而是要判 是要判断 要判断它 判断它从 断它从哪 它从哪里 从哪里来 是否 否被 被执 执行 行过 是否被 否被执 被执行 执行过 是否被执 否被执行 被执行过 否还 还有 有持 持久 久化 化或 或横 横向 向痕 痕迹 是否还 否还有 还有持 有持久 持久化 久化或 化或横 或横向 横向痕 向痕迹 是否还有 否还有持 还有持久 有持久化 持久化或 久化或横 化或横向 或横向痕 横向痕迹 这篇 篇文 文章 章按 按一 次相 相对 对完 完整 整的 查链 链路 路来 来写 这篇文 篇文章 文章按 章按一 按一次 一次相 次相对 相对完 对完整 完整的 整的排 排查链 查链路 链路来 路来写 这篇文章 篇文章按 文章按一 章按一次 按一次相 一次相对 次相对完 相对完整 对完整的 完整的排 整的排查 的排查链 排查链路 查链路来 链路来写 重点 点放 放在 在防 防守 守视 视角 角的 的取 取证 重点放 点放在 放在防 在防守 防守视 守视角 视角的 角的取 的取证 重点放在 点放在防 放在防守 在防守视 防守视角 守视角的 视角的取 角的取证 日志 志关 关联 日志关 志关联 日志关联 代码 码审 审计 计和 和加 加固 固建 建议 议上 代码审 码审计 审计和 计和加 和加固 加固建 固建议 建议上 代码审计 码审计和 审计和加 计和加固 和加固建 加固建议 固建议上 不涉 涉及 及攻 攻击 击利 利用 用细 细节 不涉及 涉及攻 及攻击 攻击利 击利用 利用细 用细节 不涉及攻 涉及攻击 及攻击利 攻击利用 击利用细 利用细节 问题 题场 景已 已知 知信 信息 息比 比较 较有 有限 问题场 题场景 场景已 景已知 已知信 知信息 信息比 息比较 比较有 较有限 问题场景 题场景已 场景已知 景已知信 已知信息 知信息比 信息比较 息比较有 比较有限 器为 务器为 服务器为 运行 务目 目录 录存 在近 近期 期新 新增 增的 的可 业务目 务目录 目录存 录存在 存在近 在近期 近期新 期新增 新增的 增的可 的可疑 业务目录 务目录存 目录存在 录存在近 存在近期 在近期新 近期新增 期新增的 新增的可 增的可疑 示文 件具 具有 提示文 示文件 文件具 件具有 备提示文 提示文件 示文件具 文件具有 特征 务方 方不 不确 确定 定是 否存 在异 异常 常登 登录 录或 或数 数据 据泄 泄露 业务方 务方不 方不确 不确定 确定是 定是否 是否存 否存在 存在异 在异常 异常登 常登录 登录或 录或数 或数据 数据泄
  12. SSRF(Server-Side Request Forgery,服务器端请求伪造)并不是一个只靠“禁止访问内网 IP”就能彻底解决的问题。实际审计中,漏洞往往出现在图片抓取、Webhook 回调、文档转换、远程资源导入、健康检查等功能里。开发者通常只关注了参数是否为合法 URL,却忽略了 URL 解析差异、DNS 解析变化、重定向链以及非 HTTP 协议等边界情况。 本文以代码审计和修复验证为主线,整理一套适用于 Web 应用的 SSRF 分析方法。示例均以本地测试服务或受控地址为前提,不涉及访问真实内网资产、云平台元数据或第三方系统。 一、典型业务场景与风险边界常见的 SSRF 入口包括以下几类: 根据用户提交的 URL 下载头像、附件或远程图片。服务端生成 PDF、截图或预览页面时访问指定地址。Webhook、回调地址、消息推送等功能由用户配置目标 URL。导入 RSS、OpenAPI 文档或远程配置文件。后台健康检查功能允许管理员输入待检测地址。这类功能本身并不一定存在漏洞,关键在于服务端是否会按照用户可控的地址发起网络请求,以及请求是否能够到达不应暴露的网络区域。风险通常包括内部管理面板暴露、服务端口探测、云环境敏感接口访问、内网服务未授权调用以及敏感响应回显。 审计时应区分“服务端可以发起请求”和“攻击者可以利用该请求访问敏感资源”两个层次。前者是行为特征,后者才是需要重点确认的安全影响。二、代码审计时优先关注的危险模式以 Python 为例,下面的代码虽然简短,但将用户输入直接交给网络库,风险边界完全由输入决定: import requests def fetch_remote(url): response = requests.get(url, timeout=10) return response.content审计时不要只搜索 requests.get。还应关注以下调用链: HTTP 客户端:requests、urllib、httpx、Java 的 URL、HttpClient、Go 的 http.Get 等。间接访问:图片处理器、HTML 转 PDF、截图组件、文档解析器和第三方 SDK。协议处理:file、ftp、gopher 等非预期协议是否被底层库支持。重定向逻辑:请求库是否默认跟随 30x,重定向后的地址是否重新校验。响应处理:是否将响应内容原样返回、写入本地文件,或交给后续解析器。建议从路由入口开始,沿着“参数接收—URL 解析—DNS 解析—建立连接—跟随重定向—处理响应”的链路逐段确认,而不是只在某个危险函数附近做字符串匹配。 三、URL 校验中最容易出现的误区1. 只判断字符串前缀类似 url.startswith(' 的校验并不可靠。合法的 URL 语法允许用户名、端口、路径和编码字符参与解析,单纯的前缀判断无法准确表示目标主机。 2. 只检查一次 DNS 结果如果应用先解析域名判断是否为公网地址,之后网络库又重新解析一次,就可能出现判断结果与实际连接目标不一致的情况。DNS 记录变化、解析器差异和缓存策略都可能造成这种问题。 3. 只禁止几个常见内网地址需要同时考虑 IPv4、IPv6、回环地址、链路本地地址、私有地址、保留地址以及 IPv4 映射的 IPv6 地址。仅过滤 127.0.0.1、10.0.0.0/8 等少数网段,容易留下遗漏。 4. 校验原始地址,却信任重定向即使初始地址是允许的公网域名,目标服务器也可能返回一个指向受限地址的重定向。安全策略应在每一次跳转前重新解析并校验目标,并限制最大跳转次数。 四、一个更稳妥的校验思路生产环境不建议自行拼装复杂的 URL 黑名单。更可靠的方式是采用“协议白名单、主机白名单、解析后地址校验、连接过程限制”组合策略。 下面是一个用于说明思路的 Python 示例。它不负责下载内容,只展示如何在请求前对目标进行约束。实际项目中还需要结合网络出口策略和统一 HTTP 客户端实现。 from ipaddress import ip_address, ip_network from urllib.parse import urlparse import socket ALLOWED_SCHEMES = {'https'} ALLOWED_HOSTS = {'static.example.test'} BLOCKED_NETS = [ ip_network('10.0.0.0/8'), ip_network('172.16.0.0/12'), ip_network('192.168.0.0/16'), ip_network('127.0.0.0/8'), ip_network('169.254.0.0/16'), ip_network('::1/128'), ip_network('fc00::/7'), ip_network('fe80::/10'), ] def is_blocked_ip(value): ip = ip_address(value) return any(ip in network for network in BLOCKED_NETS) or ip.is_private or ip.is_loopback or ip.is_link_local def validate_target(raw_url): parsed = urlparse(raw_url) if parsed.scheme not in ALLOWED_SCHEMES: raise ValueError('unsupported scheme') if parsed.username or parsed.password: raise ValueError('userinfo is not allowed') if not parsed.hostname or parsed.port not in (None, 443): raise ValueError('invalid host or port') if parsed.hostname.lower() not in ALLOWED_HOSTS: raise ValueError('host is not allowlisted') addresses = socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM) for item in addresses: resolved = item[4][0] if is_blocked_ip(resolved): raise ValueError('resolved address is not allowed') return parsed这个示例仍然不是完整的通用组件,原因在于 DNS 校验和实际建连之间可能存在时间窗口。因此更推荐以下方案: 优先使用固定域名白名单,而不是允许任意公网域名。对允许访问的域名配置固定解析结果,或通过受控代理统一转发。禁止用户指定端口,必要时仅允许 443 或明确的业务端口。禁用自动重定向,或者对每次重定向重新执行完整校验。设置连接超时、读取超时、响应大小上限和最大响应头长度。将发起请求的服务放置在隔离网络中,出口防火墙默认拒绝访问管理网段。五、修复验证的安全流程验证 SSRF 修复时,应使用自己控制的测试环境,不要直接探测生产内网或第三方地址。可以准备一个简单的本地 HTTP 服务,用于记录请求方法、路径、请求头和来源地址: python3 -m http.server 18080 --bind 127.0.0.1然后在测试应用中使用以下几类受控输入验证策略是否生效: 合法的白名单 HTTPS 域名,确认正常业务不受影响。HTTP、FTP 或其他非白名单协议,确认被拒绝。带用户名密码、非标准端口或异常编码的 URL,确认解析结果符合预期。解析到回环、私有、链路本地和 IPv6 保留地址的测试域名,确认被拒绝。返回重定向的测试服务,确认跳转目标会重新校验。超大响应、慢响应和连接失败场景,确认不会造成资源耗尽。验证重点不是“某个 Payload 能否成功访问”,而是确认所有不符合业务策略的目标都会在请求发出前被拦截,并且错误信息不会向外暴露内部解析结果、网络拓扑或服务版本。 六、日志与监控同样重要SSRF 防护不能只依赖代码。建议对服务端主动请求建立结构化日志,但避免记录完整的敏感查询参数。日志至少可以包含请求功能、目标域名、解析地址类别、是否命中策略、失败原因和耗时。 在网络层,可以监控应用服务器是否突然访问管理网段、云平台保留地址或大量非常规端口。对于图片抓取、Webhook 等高频功能,还应设置单用户和单租户的请求速率限制,防止攻击者利用合法功能进行内部端口探测或资源消耗。 七、常见修复失败案例在控制器中做了校验,但另一个异步任务入口没有复用同一套策略。只校验页面展示的 URL,后台下载器仍然直接使用原始参数。过滤了 IPv4 私网段,却遗漏 IPv6 和 IPv4 映射地址。限制了初始 URL,却让 HTTP 客户端自动跟随重定向。使用黑名单维护所有危险地址,随着网络环境变化逐渐失效。应用层校验通过后,容器或主机网络仍可访问不必要的管理服务。修复时应将策略封装成统一组件,禁止业务代码自行调用底层网络库。对于确实需要访问任意公网资源的场景,可以考虑部署独立的出站代理,在代理层执行域名解析、地址分类、协议限制和审计记录。 总结SSRF 的核心问题不是某个字符串绕过技巧,而是“用户输入是否能够影响服务端的网络边界”。高质量审计需要同时观察代码调用链、URL 解析行为、DNS 与连接过程、重定向处理、响应使用方式以及底层网络隔离。 实践中,最稳妥的防护组合通常是:默认拒绝、明确的协议和域名白名单、解析后地址校验、禁止或严格控制重定向、限制端口与响应资源,并通过出口防火墙提供最后一道边界。完成修复后,还要使用受控测试环境覆盖 IPv4、IPv6、重定向、超时和异常解析等场景,避免只修复一个入口,却把同样的风险留在其他业务链路中。 代码审计、调用链与关键函数定位示意 服务 务器 器端 端请 请求 求伪 伪造 服务器 务器端 器端请 端请求 请求伪 求伪造 服务器端 务器端请 器端请求 端请求伪 请求伪造 并不 不是 是一 一个 个只 只靠 并不是 不是一 是一个 一个只 个只靠 并不是一 不是一个 是一个只 一个只靠 禁止 止访 访问 问内 内网 禁止访 止访问 访问内 问内网 禁止访问 止访问内 访问内网 就能 能彻 彻底 底解 解决 决的 的问 问题 就能彻 能彻底 彻底解 底解决 解决的 决的问 的问题 就能彻底 能彻底解 彻底解决 底解决的 解决的问 决的问题 实际 际审 审计 计中 实际审 际审计 审计中 实际审计 际审计中 漏洞 洞往 往往 往出 出现 现在 在图 图片 片抓 抓取 漏洞往 洞往往 往往出 往出现 出现在 现在图 在图片 图片抓 片抓取 漏洞往往 洞往往出 往往出现 往出现在 出现在图 现在图片 在图片抓 图片抓取 回调 文档 档转 转换 文档转 档转换 文档转换 远程 程资 资源 源导 导入 远程资 程资源 资源导 源导入 远程资源 程资源导 资源导入 健康 康检 检查 查等 等功 功能 能里 健康检 康检查 检查等 查等功 等功能 功能里 健康检查 康检查等 检查等功 查等功能 等功能里 开发 发者 者通 通常 常只 只关 关注 注了 了参 参数 数是 是否 否为 为合 合法 开发者 发者通 者通常 通常只 常只关 只关注 关注了 注了参 了参数 参数是 数是否 是否为 否为合 为合法 开发者通 发者通常 者通常只 通常只关 常只关注 只关注了 关注了参 注了参数 了参数是 参数是否 数是否为 是否为合 否为合法 却忽 忽略 略了 却忽略 忽略了 却忽略了 解析 析差 差异 解析差 析差异 解析差异 析变 变化 解析变 析变化 解析变化 重定 定向 向链 链以 以及 及非 重定向 定向链 向链以 链以及 以及非 重定向链 定向链以 向链以及 链以及非 协议 议等 等边 边界 界情 情况 协议等 议等边 等边界 边界情 界情况 协议等边 议等边界 等边界情 边界情况 本文 文以 以代 代码 码审 计和 和修 修复 复验 验证 证为 为主 主线 本文以 文以代 以代码 代码审 码审计 审计和 计和修 和修复 修复验 复验证 验证为 证为主 为主线 本文以代 文以代码 以代码审 代码审计 码审计和 审计和修 计和修复 和修复验 修复验证 复验证为 验证为主 证为主线 整理 理一 一套 套适 适用 用于 整理一 理一套 一套适 套适用 适用于 整理一套 理一套适 一套适用 套适用于 应用 用的 应用的 分析 析方 方法 分析方 析方法 分析方法 示例 例均 均以 以本 本地 地测 测试 试服 务或 或受 受控 控地 地址 址为 为前 前提 示例均 例均以 均以本 以本地 本地测 地测试 测试服 试服务 服务或 务或受 或受控 受控地 控地址 地址为 址为前 为前提 示例均以 例均以本 均以本地 以本地测 本地测试 地测试服 测试服务 试服务或 服务或受 务或受控 或受控地 受控地址 控地址为 地址为前 址为前提 不涉 涉及 及访 问真 真实 实内 网资 资产 不涉及 涉及访 及访问 访问真 问真实 真实内 实内网 内网资 网资产 不涉及访 涉及访问 及访问真 访问真实 问真实内 真实内网 实内网资 内网资产 云平 平台 台元 元数 数据 据或 或第 第三 三方 方系 系统 云平台 平台元 台元数 元数据 数据或 据或第 或第三 第三方 三方系 方系统 云平台元 平台元数 台元数据 元数据或 数据或第 据或第三 或第三方 第三方系 三方系统 典型 型业 业务 务场 场景 景与 与风 风险 险边 界常 常见 见的 典型业 型业务 业务场 务场景 场景与 景与风 与风险 风险边 险边界 边界常 界常见 常见的 典型业务 型业务场 业务场景 务场景与 场景与风 景与风险 与风险边 风险边界 险边界常 边界常见 界常见的 入口 口包 包括 括以 以下 下几 几类 入口包 口包括 包括以 括以下 以下几 下几类 入口包括 口包括以 包括以下 括以下几 以下几类 根据 据用 用户 户提 提交 交的 根据用 据用户 用户提 户提交 提交的 根据用户 据用户提 用户提交 户提交的 下载 载头 头像 下载头 载头像 下载头像 附件 件或 或远 程图 附件或 件或远 或远程 远程图 程图片 附件或远 件或远程 或远程图 远程图片 务端 端生 生成 服务端 务端生 端生成 服务端生 务端生成 截图 图或 或预 预览 览页 页面 面时 时访 问指 指定 定地 截图或 图或预
  13. 背景SSRF(Server-Side Request Forgery)这类问题在代码审计和漏洞复现里很常见,但真正处理起来并不只是“禁用内网地址”这么简单。很多业务会天然需要服务端发起请求,例如:远程图片抓取、Webhook 回调、在线文档预览、URL 健康检查、第三方接口转发等。如果只做简单黑名单,通常很容易被各种地址表示法、跳转链、DNS 解析变化绕过去。 这篇文章整理一个偏工程化的 SSRF 分析方法,重点放在安全验证、漏洞定位和加固落地。内容不会展开攻击性利用,只讨论如何识别风险、复现边界和修复设计。 典型场景下面这些接口在审计中需要重点关注: 根据用户提交的 URL 抓取图片、头像、封面、文件。Webhook、Callback、OpenAPI 回调地址配置。在线代理、站点截图、URL 预览、网页转 PDF。后端请求第三方资源,并将响应内容返回给前端。文件导入功能支持 HTTP/HTTPS 远程地址。一个常见的风险接口可能长这样: POST /api/fetch-image HTTP/1.1 Content-Type: application/json { "url": " }如果服务端直接拿这个 url 发请求,并把结果返回或保存,就需要考虑 SSRF 风险。 问题本质SSRF 的核心不是“请求了一个 URL”,而是“用户可控输入影响了服务端的网络访问目标”。服务端一般处在更高权限的网络位置,可能能访问到公网用户无法访问的内部服务、云厂商元数据接口、管理面板、Redis、Elasticsearch、Kubernetes API 等。 从代码角度看,危险模式通常是: // 伪代码,仅用于说明风险模式 url := request.json["url"] resp := httpClient.Get(url) return resp.Body这段代码的问题不在于使用了 HTTP 客户端,而在于没有对访问目标、协议、解析结果、跳转行为和响应体做任何约束。 技术分析1. URL 解析不一致不同语言、不同库对 URL 的解析行为不完全一致。业务代码校验时使用一个解析器,真正请求时 HTTP 客户端又使用另一个解析器,就可能产生判断差异。审计时要确认校验和请求是否基于同一套解析结果。 建议至少检查以下字段: scheme:只允许 http、https。host:必须存在,且不能是空值。port:只允许业务必要端口,例如 80、443。userinfo:不建议允许,例如 URL 中的特殊字符、编码、大小写、尾点域名、IPv6 表示等情况。修复时不要依赖字符串 contains、startsWith 这类简单判断。 2. DNS 解析与地址校验只校验域名字符串是不够的。安全判断应该发生在 DNS 解析之后,对解析得到的 IP 地址进行分类检查。需要拒绝的地址范围一般包括: loopback:127.0.0.0/8、::1。private:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。link-local:169.254.0.0/16、fe80::/10。multicast、reserved、unspecified。云元数据服务常见地址,例如 169.254.169.254。另外要注意 DNS Rebinding。也就是第一次解析通过校验,实际请求时又解析到内部地址。稳妥做法是:由应用自己解析域名并校验 IP,然后建立连接时绑定到已校验的 IP,同时保留原始 Host/SNI 的业务需求;或者在网络层做 egress 策略,避免应用层校验成为唯一防线。 3. 跳转链风险很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只检查初始 URL,不检查跳转后的 Location,仍然存在风险。建议禁用自动跳转,手动处理每一次 Location,并对新的 URL 重复完整校验。 // Go 示例:禁用自动跳转,避免只校验首个 URL client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }如果业务确实需要跳转,建议限制最大跳转次数,例如不超过 3 次,并要求每次跳转后的 scheme、host、解析 IP 都通过校验。 4. 协议与客户端能力SSRF 不一定只发生在 HTTP。部分库或组件支持 file、gopher、ftp、dict、jar 等协议,或者在某些封装层里会自动处理特殊协议。实际审计时要看最终调用的客户端支持什么,不要只看接口名。 一般建议: 明确只允许 http 和 https。拒绝非标准端口,除非业务确实需要。不要把用户输入直接传给支持多协议的通用下载器。对响应类型和大小做限制,避免被当成内网探测或大文件下载通道。关键步骤:一次安全复现与确认流程为了避免测试行为影响线上环境,建议在本地或测试环境搭建最小复现。流程如下: 1. 定位数据流先确认用户输入从入口到服务端请求的位置。代码审计时重点搜索: curl_exec file_get_contents fsockopen requests.get urllib.request http.Get RestTemplate WebClient OkHttpClient HttpClient同时关注图片处理、导入导出、回调配置等业务代码,不要只搜“url”。很多变量名可能是 avatar、callback、endpoint、source、remotePath。 2. 在测试环境验证外连能力可以准备一个自己控制的测试 HTTP 服务,用于确认服务端是否会访问用户提交的地址。这里只做外连确认,不进行内网探测。 python3 -m http.server 8080提交测试 URL 后观察测试服务日志。如果能看到服务端请求,说明入口具备服务端请求能力,后续需要检查限制是否充分。 3. 验证过滤边界在授权测试环境里,可以围绕以下点做边界验证: 是否限制协议,只允许 http/https。是否允许跳转,跳转后的地址是否重新校验。是否校验 DNS 解析后的 IP。IPv4、IPv6、域名尾点、大小写、编码后 URL 是否处理一致。是否限制响应大小、超时时间、Content-Type。这里的重点是验证防护逻辑是否完整,而不是扩大影响面。生产环境不建议做内网地址访问测试。 加固方案1. 优先使用白名单如果业务场景固定,例如只允许访问企业自己的对象存储域名、固定第三方 API 域名,白名单是最可靠的方式。不要用“禁止 127.0.0.1、localhost”这类黑名单作为主要防线。 // 伪代码:基于业务白名单校验 host allowedHosts := map[string]bool{ "img.example-cdn.com": true, "api.partner.example": true, } if !allowedHosts[parsed.Hostname()] { return error("host not allowed") }如果必须允许用户输入任意公网 URL,则需要组合使用 URL 规范化、DNS 解析校验、跳转校验、网络出口限制。 2. 解析后校验 IP 范围校验逻辑应基于解析后的 IP,而不是原始字符串。下面是一个偏思路化的 Go 示例: func isBlockedIP(ip net.IP) bool { if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() { return true } // 额外阻断云元数据地址 if ip.String() == "169.254.169.254" { return true } return false }实际项目里还要补充 IPv6、保留地址段和公司内部网段。不要只判断字符串前缀。 3. 控制 HTTP 客户端行为设置较短超时,例如连接超时 2 秒、整体超时 5 秒。禁用自动跳转,或对每次跳转重新校验。限制最大响应体大小,例如 5MB。限制 Content-Type,例如只允许 image/png、image/jpeg。禁止携带内部凭据、Cookie、Authorization 头。不要把响应体原样返回给用户,避免变成代理通道。// Go 示例:限制响应体大小 resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() limited := io.LimitReader(resp.Body, 5*1024*1024) body, err := io.ReadAll(limited)4. 网络层兜底应用层校验容易因为边界情况遗漏,所以建议配合网络层策略: 容器或主机层限制出站访问,只允许访问必要公网地址。云环境中阻断对元数据服务的非必要访问。使用独立的下载服务,放在低权限网络区域。对服务端主动外连建立审计日志,包括目标域名、解析 IP、状态码、耗时。在 Kubernetes 环境,可以使用 NetworkPolicy 限制 Pod 出站访问。具体策略要结合 CNI 能力和业务依赖设计,不能只写一条“允许全部”。 代码审计检查清单检查项关注点建议URL 来源是否由用户完全可控优先改为白名单或后端固定配置协议限制是否允许 file、ftp 等非 HTTP 协议只允许 http/httpsDNS 校验是否只判断域名字符串解析后校验 IP 分类跳转处理是否默认跟随 30x禁用自动跳转或逐跳校验响应限制是否无限读取响应体限制大小、类型和超时凭据泄露是否转发 Cookie、Token外连请求使用干净 Header网络出口服务是否能访问内网敏感段通过防火墙或 NetworkPolicy 兜底常见误区只过滤 localhost。实际还有 127.0.0.1、::1、内网段、link-local、各种域名解析情况。只在前端校验 URL。服务端必须独立校验。只校验第一次请求。跳转后的地址同样需要校验。认为 HTTPS 就安全。HTTPS 只能保证传输安全,不代表目标地址可信。把 SSRF 当成单点漏洞修补。更合理的是把外连能力当成高风险能力统一治理。事件复盘建议如果已经发现 SSRF 风险,复盘时建议关注三条线: 影响面:哪些接口存在服务端外连能力,是否可被未授权或低权限用户触发。可达性:相关服务能访问哪些内部网段、云元数据、管理接口。数据暴露:接口是否会返回响应体、响应头、错误信息或时间差。日志侧建议检索异常目标地址、短时间大量不同 URL、访问失败率异常、非业务域名请求等。对于历史日志不足的系统,至少在修复后补充外连审计,方便后续追踪。 总结SSRF 的治理重点不是写一个复杂正则,而是建立完整的访问目标控制链路:输入规范化、协议限制、白名单优先、DNS 解析后校验、跳转逐次校验、HTTP 客户端收敛、网络出口兜底。单靠某一层很难覆盖所有边界。 从长期维护角度看,建议把“服务端主动访问外部 URL”抽象成统一组件,业务侧不要各自实现下载逻辑。统一组件里沉淀校验、日志、超时、响应大小限制和安全策略,后续审计和加固都会轻松很多。 服务器、云资产与权限边界梳理示意 背景 这类 类问 问题 题在 在代 代码 码审 审计 计和 和漏 漏洞 洞复 复现 现里 里很 很常 常见 这类问 类问题 问题在 题在代 在代码 代码审 码审计 审计和 计和漏 和漏洞 漏洞复 洞复现 复现里 现里很 里很常 很常见 这类问题 类问题在 问题在代 题在代码 在代码审 代码审计 码审计和 审计和漏 计和漏洞 和漏洞复 漏洞复现 洞复现里 复现里很 现里很常 里很常见 但真 真正 正处 处理 理起 起来 来并 并不 不只 只是 但真正 真正处 正处理 处理起 理起来 起来并 来并不 并不只 不只是 但真正处 真正处理 正处理起 处理起来 理起来并 起来并不 来并不只 并不只是 禁用 用内 内网 网地 地址 禁用内 用内网 内网地 网地址 禁用内网 用内网地 内网地址 这么 么简 简单 这么简 么简单 这么简单 很多 多业 业务 务会 会天 天然 然需 需要 要服 服务 务端 端发 发起 起请 请求 很多业 多业务 业务会 务会天 会天然 天然需 然需要 需要服 要服务 服务端 务端发 端发起 发起请 起请求 很多业务 多业务会 业务会天 务会天然 会天然需 天然需要 然需要服 需要服务 要服务端 服务端发 务端发起 端发起请 发起请求 例如 远程 程图 图片 片抓 抓取 远程图 程图片 图片抓 片抓取 远程图片 程图片抓 图片抓取 回调 在线 线文 文档 档预 预览 在线文 线文档 文档预 档预览 在线文档 线文档预 文档预览 健康 康检 检查 健康检 康检查 健康检查 第三 三方 方接 接口 口转 转发 发等 第三方 三方接 方接口 接口转 口转发 转发等 第三方接 三方接口 方接口转 接口转发 口转发等 如果 果只 只做 做简 单黑 黑名 名单 如果只 果只做 只做简 做简单 简单黑 单黑名 黑名单 如果只做 果只做简 只做简单 做简单黑 简单黑名 单黑名单 通常 常很 很容 容易 易被 被各 各种 种地 址表 表示 示法 通常很 常很容 很容易 容易被 易被各 被各种 各种地 种地址 地址表 址表示 表示法 通常很容 常很容易 很容易被 容易被各 易被各种 被各种地 各种地址 种地址表 地址表示 址表示法 跳转 转链 跳转链 解析 析变 变化 化绕 绕过 过去 解析变 析变化 变化绕 化绕过 绕过去 解析变化 析变化绕 变化绕过 化绕过去 这篇 篇文 文章 章整 整理 理一 一个 个偏 偏工 工程 程化 化的 这篇文 篇文章 文章整 章整理 整理一 理一个 一个偏 个偏工 偏工程 工程化 程化的 这篇文章 篇文章整 文章整理 章整理一 整理一个 理一个偏 一个偏工 个偏工程 偏工程化 工程化的 分析 析方 方法 分析方 析方法 分析方法 重点 点放 放在 在安 安全 全验 验证 重点放 点放在 放在安 在安全 安全验 全验证 重点放在 点放在安 放在安全 在安全验 安全验证 洞定 定位 位和 和加 加固 固落 落地 漏洞定 洞定位 定位和 位和加 和加固 加固落 固落地 漏洞定位 洞定位和 定位和加 位和加固 和加固落 加固落地 内容 容不 不会 会展 展开 开攻 攻击 击性 性利 利用 内容不 容不会 不会展 会展开 展开攻 开攻击 攻击性 击性利 性利用 内容不会 容不会展 不会展开 会展开攻 展开攻击 开攻击性 攻击性利 击性利用 只讨 讨论 论如 如何 何识 识别 别风 风险 只讨论 讨论如 论如何 如何识 何识别 识别风 别风险 只讨论如 讨论如何 论如何识 如何识别 何识别风 识别风险 现边 边界 界和 和修 修复 复设 设计 复现边 现边界 边界和 界和修 和修复 修复设 复设计 复现边界 现边界和 边界和修 界和修复 和修复设 修复设计 典型 型场 场景 景下 下面 面这 这些 些接 口在 在审 计中 中需 要重 点关 关注 典型场 型场景 场景下 景下面 下面这 面这些 这些接 些接口 接口在 口在审 在审计 审计中 计中需 中需要 需要重 要重点 重点关 点关注 典型场景 型场景下 场景下面 景下面这 下面这些 面这些接 这些接口 些接口在 接口在审 口在审计 在审计中 审计中需 计中需要 中需要重 需要重点 要重点关 重点关注 根据 据用 用户 户提 提交 交的 根据用 据用户 用户提 户提交 提交的 根据用户 据用户提 用户提交 户提交的 取图 抓取图 取图片 抓取图片 头像 封面 文件 调地 址配 配置 回调地 调地址 地址配 址配置 回调地址 调地址配 地址配置 线代 代理 在线代 线代理 在线代理 站点 点截 截图 站点截 点截图 站点截图 网页 页转 网页转 后端 端请 求第
  14. SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“服务端发起了一次 HTTP 请求”这么简单。真正需要关注的是:用户可控数据是否进入了服务端的网络请求链路,服务端是否能够访问不应暴露的网络区域,以及应用、代理和云环境之间是否存在边界失效。 这类问题在代码审计中比较常见,尤其容易出现在图片抓取、Webhook 调试、文档转换、远程资源导入、URL 预览和第三方接口探测等功能中。本文以防守视角整理一套可复用的方法,重点放在审计思路、低风险验证、修复设计和复测证据上。 一、问题场景与风险边界典型业务流程是:用户提交一个 URL,服务端下载内容后进行解析、转码或展示。风险通常来自以下几类位置: 直接调用 HTTP 客户端,例如 requests、urllib、OkHttp、axios 或内部封装的 fetch 方法。用户输入先经过重定向、短链接或 URL 解析,再由后台任务异步访问。文件解析器、Markdown 渲染器、网页截图服务间接触发外部请求。代理服务允许访问内部域名、管理接口或容器网络。应用层做了域名校验,但 DNS 解析、重定向和底层连接过程没有保持一致。SSRF 的影响不应只用“能否访问本机”来判断。审计时至少要区分三种边界: 应用自身边界:是否能访问仅绑定在回环地址的管理接口或调试接口。网络边界:是否能访问内网网段、容器网段、办公网或测试环境。身份边界:请求是否会自动携带服务端的 Cookie、内部鉴权头或云环境凭证。验证过程中只使用自建测试服务和明确授权的测试网段,不访问真实云元数据、生产管理接口或第三方内部资源。审计的目标是证明边界存在,而不是扩大影响范围。 二、代码审计:从输入点追踪到请求出口审计 SSRF 时,不要只搜索“url”变量。更有效的方法是从外部输入开始,沿着数据流追踪到网络请求出口。 第一步是定位输入点,包括 HTTP 参数、JSON 字段、导入文件中的 URL、消息队列字段以及后台任务参数。第二步是搜索请求出口,例如 requests.get、httpx.Client、urllib.request.urlopen、Java 的 URLConnection、Go 的 http.Client.Do 等。第三步是检查两者之间是否存在完整且一致的校验。 下面是一个典型的高风险模式。问题不在于使用了某个具体库,而在于用户输入未经边界检查就进入了服务端请求函数: def fetch_preview(user_url): response = requests.get(user_url, timeout=5, allow_redirects=True) return response.text[:20000]审计时建议继续向下确认几个细节: 是否允许 http、https 之外的协议。是否允许显式端口,是否限制了端口范围。是否校验了域名解析结果,而不是只校验字符串。是否跟随重定向,重定向后的目标是否重新校验。是否存在代理环境变量、服务网格或网关的二次转发。是否会把响应正文、响应头或错误信息返回给用户。请求是否自动附带 Cookie、Authorization 或内部服务头。三、低风险验证方法验证时不需要直接探测真实内网。可以在本地或隔离测试环境启动一个简单的 HTTP 服务,分别模拟正常外部站点、禁止访问的测试地址以及重定向场景。 例如,先启动一个仅用于测试的本地服务: python3 -m http.server 18080 --bind 127.0.0.1然后在应用的测试环境中提交一个指向该服务的 URL,观察服务端访问日志。验证重点包括: 请求是否确实由服务端发出,而不是浏览器直接访问。应用是否允许访问回环地址。应用是否限制了非标准端口。应用是否将响应内容或状态信息返回给调用方。应用日志中是否记录了原始 URL、最终 URL、解析地址和拒绝原因。对于重定向测试,应在自建服务中配置一个跳转到另一个测试地址的响应,确认应用是否对每一次跳转重新执行校验。不要把测试目标替换成真实的内网服务,也不要使用云厂商元数据地址。 四、URL 校验中最容易遗漏的细节1. 协议白名单如果业务只需要抓取网页,通常应只允许 http 和 https。file、ftp、gopher 等协议会扩大攻击面,除非业务有明确需求,否则应在解析阶段拒绝。 2. 用户信息与端口URL 中可能包含用户名、密码和显式端口。即便客户端库能够正常解析,也不代表业务应该接受这些字段。建议拒绝 URL 中的 username 和 password,并对端口设置明确白名单。 3. IP 字面量与地址类型不能只用字符串判断目标是否为内网地址。IPv4、IPv6、IPv4-mapped IPv6、回环地址、链路本地地址、组播地址和未指定地址都应纳入检查。还要注意十进制、十六进制或其他非标准表示方式可能被不同组件以不同方式解释。 4. DNS 解析与重绑定“先解析、后请求”的简单实现仍然可能存在时间窗口:校验阶段解析到公网地址,真正建立连接时又解析到了内部地址。更稳妥的方式是通过统一的出站代理完成解析和连接,并在代理侧执行网络策略;如果必须由应用直连,则需要尽量复用已校验的解析结果,同时在网络层限制出站范围。 5. 重定向首次 URL 校验通过并不代表整个请求链路安全。每次重定向都必须重新解析、重新检查协议、域名、IP 和端口,并设置最大跳转次数。更简单的做法是默认关闭自动重定向,只有在业务确实需要时才受控开启。 五、一个防守型校验示例下面的示例用于说明校验顺序,不应被当作完整的网络安全边界。生产环境还需要配合出站防火墙、代理策略、连接超时和响应大小限制。 from ipaddress import ip_address from socket import getaddrinfo, AF_UNSPEC from urllib.parse import urlsplit ALLOWED_SCHEMES = {'http', 'https'} ALLOWED_PORTS = {80, 443} def resolve_addresses(hostname): results = set() for item in getaddrinfo(hostname, None, family=AF_UNSPEC, type=0): results.add(item[4][0]) return results def validate_target(raw_url): parsed = urlsplit(raw_url) if parsed.scheme.lower() not in ALLOWED_SCHEMES: raise ValueError('scheme is not allowed') if not parsed.hostname: raise ValueError('hostname is required') if parsed.username or parsed.password: raise ValueError('userinfo is not allowed') try: port = parsed.port or (443 if parsed.scheme.lower() == 'https' else 80) except ValueError: raise ValueError('invalid port') if port not in ALLOWED_PORTS: raise ValueError('port is not allowed') addresses = resolve_addresses(parsed.hostname) if not addresses: raise ValueError('hostname cannot be resolved') for item in addresses: ip = ip_address(item) if (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified or ip.is_reserved): raise ValueError('target address is not allowed') return parsed这个示例有几个需要特别说明的地方: 校验对象是解析后的结构,而不是简单字符串匹配。同时检查所有解析结果,不能只检查第一个地址。解析通过后,不能再无控制地让底层库重新解析原始域名。如果存在代理,必须确认代理是否会重新解析域名,以及代理本身的出站策略。IPv6 环境下应明确测试地址族,不能假设所有请求都走 IPv4。在实际项目中,更推荐将“目标解析、策略判断和建立连接”统一下沉到一个受控的出站组件中。应用只提交经过业务约束的目标,网络组件负责最终的域名解析、地址过滤、重定向处理和出站审计。 六、请求层的附加防护仅做地址校验通常还不够,建议同时设置以下限制: 连接超时、读取超时和总请求时长,避免请求占满工作线程。最大响应体积,避免通过远程资源消耗内存和磁盘。最大重定向次数,并对每次跳转重新执行策略检查。禁止自动发送系统环境中的代理凭据、Cookie 和 Authorization 头。限制响应内容类型,只允许业务真正需要的 MIME 类型。对后台任务设置并发上限和队列长度,防止被用作内部网络扫描器。出站防火墙默认拒绝,仅放行明确需要的目标网段和端口。如果业务是图片、文档或网页抓取,还应考虑解析器自身的风险。例如,下载阶段限制了 URL,并不代表后续的 SVG、XML、CSS 或 HTML 解析阶段不会再次发起外部请求。应分别检查下载器、解压器、渲染器和转换器的网络访问能力。 七、日志与复测证据SSRF 修复不能只看“接口返回 400”。需要建立能够复核的证据链。建议至少记录以下字段: 请求标识、业务用户和任务来源。原始 URL 的脱敏形式。解析后的协议、主机、端口和地址族。最终连接地址或代理判定结果。拒绝原因,例如协议不允许、地址属于保留网段、端口不在白名单。重定向次数、响应状态和响应体大小。日志中不要直接记录可能包含凭据的完整 URL,也不要把远程响应原样写入日志。复测时可以建立以下用例矩阵: 正常公网域名:应按业务规则成功。回环地址和本地测试服务:应拒绝。私有网段、链路本地地址和 IPv6 回环地址:应拒绝。带用户名密码的 URL:应拒绝。非 http/https 协议:应拒绝。非白名单端口:应拒绝。跳转到禁止地址:应在跳转阶段拒绝。解析出多个地址,其中一个属于禁止范围:应整体拒绝。八、常见错误做法只检查 URL 是否以某个域名字符串开头,忽略解析边界。只在首次请求前校验,允许后续重定向自由跳转。使用黑名单拦截少量内网地址,却没有建立协议、端口和出站网络白名单。校验时使用一个 DNS 解析器,请求时又由另一个代理重新解析。认为“响应内容不返回给用户”就没有风险,忽略了内网探测、资源消耗和侧信道。只修复一个接口,没有搜索同一请求封装在其他业务中的复用位置。总结SSRF 的核心不是某个特殊 URL 写法,而是用户输入跨越了应用原本的网络信任边界。有效的治理应当同时覆盖代码审计、协议和地址校验、重定向处理、DNS 一致性、出站网络控制、凭据隔离以及日志复测。 在审计实践中,可以把问题归纳为一条稳定的检查链:输入点是否可控,是否进入请求出口,请求是否经过统一策略,解析与连接是否一致,重定向是否重新校验,网络层是否有最终阻断,日志是否能够证明策略确实生效。按照这条链路推进,通常比单纯收集各种绕过写法更容易定位根因,也更有利于形成可长期维护的修复方案。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不是 并不是 端发 发起 起了 了一 一次 务端发 端发起 发起了 起了一 了一次 服务端发 务端发起 端发起了 发起了一 起了一次 这么 么简 简单 这么简 么简单 这么简单 真正 正需 需要 要关 关注 注的 的是 真正需 正需要 需要关 要关注 关注的 注的是 真正需要 正需要关 需要关注 要关注的 关注的是 用户 户可 可控 控数 数据 据是 是否 否进 进入 入了 了服 端的 的网 网络 络请 求链 链路 用户可 户可控 可控数 控数据 数据是 据是否 是否进 否进入 进入了 入了服 了服务 务端的 端的网 的网络 网络请 络请求 请求链 求链路 用户可控 户可控数 可控数据 控数据是 数据是否 据是否进 是否进入 否进入了 进入了服 入了服务 了服务端 服务端的 务端的网 端的网络 的网络请 网络请求 络请求链 请求链路 端是 否能 能够 够访 访问 问不 不应 应暴 暴露 露的 络区 区域 务端是 端是否 是否能 否能够 能够访 够访问 访问不 问不应 不应暴 应暴露 暴露的 露的网 网络区 络区域 服务端是 务端是否 端是否能 是否能够 否能够访 能够访问 够访问不 访问不应 问不应暴 不应暴露 应暴露的 暴露的网 露的网络 的网络区 网络区域 以及 及应 应用 以及应 及应用 以及应用 代理 理和 和云 云环 环境 境之 之间 间是 否存 存在 在边 边界 界失 失效 代理和 理和云 和云环 云环境 环境之 境之间 之间是 间是否 是否存 否存在 存在边 在边界 边界失 界失效 代理和云 理和云环 和云环境 云环境之 环境之间 境之间是 之间是否 间是否存 是否存在 否存在边 存在边界 在边界失 边界失效 这类 类问 问题 题在 在代 代码 码审 审计 计中 中比 比较 较常 常见 这类问 类问题 问题在 题在代 在代码 代码审 码审计 审计中 计中比 中比较 比较常 较常见 这类问题 类问题在 问题在代 题在代码 在代码审 代码审计 码审计中 审计中比 计中比较 中比较常 比较常见 尤其 其容 容易 易出 出现 现在 在图 图片 片抓 抓取 尤其容 其容易 容易出 易出现 出现在 现在图 在图片 图片抓 片抓取 尤其容易 其容易出 容易出现 易出现在 出现在图 现在图片 在图片抓 图片抓取 调试 文档 档转 转换 文档转 档转换 文档转换 远程 程资 资源 源导 导入 远程资 程资源 资源导 源导入 远程资源 程资源导 资源导入 预览 览和 和第 第三 三方 方接 接口 口探 探测 测等 等功 功能 能中 预览和 览和第 和第三 第三方 三方接 方接口 接口探 口探测 探测等 测等功 等功能 功能中 预览和第 览和第三 和第三方 第三方接 三方接口 方接口探 接口探测 口探测等 探测等功 测等功能 等功能中 本文 文以 以防 防守 守视 视角 角整 整理 理一 一套 套可 可复 复用 用的 的方 方法 本文以 文以防 以防守 防守视 守视角 视角整 角整理 整理一 理一套 一套可 套可复 可复用 复用的 用的方 的方法 本文以防 文以防守 以防守视 防守视角 守视角整 视角整理 角整理一 整理一套 理一套可 一套可复 套可复用 可复用的 复用的方 用的方法 重点 点放 放在 在审 计思 思路 重点放 点放在 放在审 在审计 审计思 计思路 重点放在 点放在审 放在审计 在审计思 审计思路 低风 风险 险验 验证 低风险 风险验 险验证 低风险验 风险验证 修复 复设 设计 计和 和复 复测 测证 证据 据上 修复设 复设计 设计和 计和复 和复测 复测证 测证据 证据上 修复设计 复设计和 设计和复 计和复测 和复测证 复测证据 测证据上 题场 场景 景与 与风 险边 界典 典型 型业 业务 务流 流程 程是 问题场 题场景 场景与 景与风 与风险 风险边 险边界 边界典 界典型 典型业 型业务 业务流 务流程 流程是 问题场景 题场景与 场景与风 景与风险 与风险边 风险边界 险边界典 边界典型 界典型业 典型业务 型业务流 业务流程 务流程是 户提 提交 交一 一个 用户提 户提交 提交一 交一个 用户提交 户提交一 提交一个 端下 下载 载内 内容 容后 后进 进行 行解 解析 务端下 端下载 下载内 载内容 内容后 容后进 后进行 进行解 行解析 服务端下 务端下载 端下载内 下载内容 载内容后 内容后进 容后进行 后进行解 进行解析 转码 码或 或展 展示 转码或 码或展 或展示 转码或展 码或展示 险通 通常 常来
  15. 背景最近在做一个内部业务系统的安全复盘时,遇到一个比较典型的场景:登录接口没有出现明显漏洞利用痕迹,但日志里能看到大量异常请求,包括高频失败登录、参数结构异常、不同 User-Agent 的批量访问,以及少量疑似探测请求。 这类问题在实际环境中很常见。它不一定意味着系统已经被入侵,但通常说明接口暴露面、认证策略、日志可观测性和风控策略存在短板。本文不讨论任何攻击利用方法,重点放在如何从防守视角复盘、分析和加固。 问题场景业务系统采用常见的前后端分离架构,登录接口大致如下: POST /api/auth/login HTTP/1.1 Content-Type: application/json { "username": "test", "password": "******", "captcha": "1234" }排查时发现几个现象: 短时间内出现大量登录失败,来源 IP 分散。部分请求缺失 captcha 字段,或字段类型不符合预期。接口返回信息区分了“用户不存在”和“密码错误”。同一账号在不同 IP 下连续失败,但应用层没有统一限速。日志记录了请求结果,但没有记录关键风险字段,后续关联分析困难。这些问题单独看都不算复杂,但组合在一起会明显提高账号体系被持续探测的风险。 技术分析1. 返回信息过于明确登录接口如果返回“用户不存在”“密码错误”“验证码错误”等过于细粒度的信息,容易暴露账号枚举面。防守上更推荐对外统一返回认证失败,只在服务端日志中记录具体原因。 // 不推荐 { "code": 1001, "message": "用户不存在" } // 推荐 { "code": 401, "message": "用户名或密码错误" }需要注意的是,统一返回并不代表丢失排查能力。正确做法是对外模糊,对内详细。 login_fail reason=user_not_found username_hash=8f14e45f ip=10.1.2.3 ua_hash=...2. 缺少分层限速很多系统只做了简单的 IP 限速,但在实际环境中,单一 IP 限速效果有限。更合理的策略是分层限速,例如: 按 IP 限速:限制单个来源的高频请求。按账号限速:限制同一账号被反复尝试。按设备指纹或会话标识限速:辅助识别异常客户端。按网段、ASN、代理特征做风险评分,而不是简单封禁。Nginx 层可以先做基础频控,应用层再做更精细的策略。 http { limit_req_zone $binary_remote_addr zone=login_ip:10m rate=5r/m; server { location /api/auth/login { limit_req zone=login_ip burst=10 nodelay; proxy_pass } } }这类配置只能作为第一层防护,不能替代应用层的账号维度风控。 3. 参数校验不完整登录接口是高频风险入口,参数校验应尽量前置。常见问题包括: username 未限制长度,导致日志污染或异常消耗。password 接受非字符串类型,触发框架边界行为。captcha 可为空或字段缺失时仍进入密码校验逻辑。JSON 解析异常被统一吞掉,导致监控无法识别异常请求。建议在进入认证逻辑前完成结构校验,并对异常类型进行分类记录。 if not isinstance(username, str) or len(username) > 128: return auth_failed() if not isinstance(password, str) or len(password) > 256: return auth_failed() if captcha_required and not valid_captcha(captcha): return auth_failed()这里的重点不是代码本身,而是避免让异常输入进入核心认证流程。 4. 日志字段不足不少系统在出现风险事件后才发现日志不够用。登录接口至少建议记录以下字段: 字段用途request_id串联网关、应用、审计日志username_hash保护隐私的同时支持账号维度分析client_ip来源分析,注意结合可信代理头user_agent_hash聚合异常客户端特征fail_reason内部排查原因,不对外返回risk_score风控策略命中情况latency_ms识别异常耗时和潜在资源消耗涉及密码、验证码、完整 Token、Cookie 的内容不要写入日志。这个问题在审计中非常常见,后续日志平台权限一旦扩大,敏感信息泄露面会被放大。 关键排查步骤1. 先确认时间线不要一上来就封 IP 或改策略,先确认异常开始时间、峰值时间、持续时间,以及是否和业务发布、营销活动、爬虫流量有关。 # 示例:按分钟统计登录失败数量 cat app.log | grep 'login_fail' | awk '{print $1" "$2}' | cut -c1-16 | sort | uniq -c如果日志是 JSON 格式,可以用 jq 做更稳定的分析。 cat app.log | jq -r 'select(.event=="login_fail") | .time[0:16]' | sort | uniq -c2. 识别异常聚合维度重点不要只看 IP。建议同时聚合以下维度: 失败次数最多的账号。单账号来源 IP 数量。单 IP 尝试账号数量。User-Agent 分布。请求体异常比例。验证码失败和密码失败的比例。# 按 username_hash 聚合失败次数 cat app.log | jq -r 'select(.event=="login_fail") | .username_hash' | sort | uniq -c | sort -nr | head3. 检查认证链路顺序认证链路建议遵循“低成本校验在前,高成本操作在后”的原则。例如: 请求方法、Content-Type 校验。JSON 结构和字段类型校验。限速和风险策略判断。验证码或二次验证校验。账号状态检查。密码哈希校验。密码哈希计算通常是相对高成本操作,如果大量异常请求都能走到这一步,可能造成不必要的资源压力。 4. 检查代理 IP 获取逻辑如果系统部署在反向代理、负载均衡或 CDN 后面,应用获取客户端 IP 时要谨慎。不能直接信任任意请求头中的 X-Forwarded-For。 # 思路示例:只信任来自可信代理的转发头 trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"] if remote_addr in trusted_proxies: client_ip = parse_x_forwarded_for(headers) else: client_ip = remote_addr如果这一步做错,限速和审计都会失真。 加固建议1. 对外响应统一登录失败统一返回认证失败,不暴露账号是否存在、是否冻结、验证码是否单独错误等细节。内部日志保留具体原因,方便排查。 2. 增加账号维度保护对连续失败的账号进行冷却,例如短时间内多次失败后要求二次验证,或者延迟再次尝试。注意不要简单锁死账号,否则可能被滥用为拒绝服务手段。 3. 引入风险评分而非单点拦截可以把 IP 信誉、失败次数、账号敏感度、设备变化、地理位置变化、请求异常程度综合为风险分。风险高时提高验证强度,而不是只依赖固定阈值。 4. 敏感日志脱敏用户名建议哈希化后记录,手机号、邮箱保留部分掩码即可。Token、密码、验证码不应进入日志。 username_hash = sha256(normalize(username) + log_salt)这里的 log_salt 建议单独管理,避免被用于反查明文。 5. 设置告警规则告警不要只看总量,也要看结构性异常。例如: 同一账号 10 分钟内失败来源超过 N 个。同一 IP 10 分钟内尝试账号超过 N 个。登录失败率突然高于历史基线。参数校验失败比例异常升高。验证码失败量与密码失败量比例异常。这类告警比单纯的 QPS 告警更有价值。 注意点不要在生产环境直接打印完整请求体,尤其是登录、支付、个人资料接口。限速策略上线前要灰度,避免误伤真实用户。账号锁定策略要谨慎,避免被利用来批量锁定他人账号。如果接入 CDN 或网关,应用层仍应保留基础校验和审计能力。排查异常流量时要区分安全风险、业务活动和监控探测,避免误判。总结登录接口安全并不只是“密码校验是否正确”这么简单,它涉及认证反馈、限速、参数校验、日志审计、风险识别和告警联动。很多线上风险不是单个高危漏洞导致的,而是多个小问题叠加后形成了可被持续消耗和探测的入口。 从长期维护角度看,建议把登录接口当作一个独立的安全资产来管理:有清晰的日志字段,有分层限速,有统一的错误响应,有可回溯的审计链路,也有可灰度调整的风控策略。这样即使遇到异常流量,也能更快判断影响范围,并采取相对稳妥的处置措施。 网络请求、日志与边界流量分析示意 背景 景最 最近 近在 在做 做一 一个 个内 内部 部业 业务 务系 系统 统的 的安 安全 全复 复盘 盘时 背景最 景最近 最近在 近在做 在做一 做一个 一个内 个内部 内部业 部业务 业务系 务系统 系统的 统的安 的安全 安全复 全复盘 复盘时 背景最近 景最近在 最近在做 近在做一 在做一个 做一个内 一个内部 个内部业 内部业务 部业务系 业务系统 务系统的 系统的安 统的安全 的安全复 安全复盘 全复盘时 遇到 到一 个比 比较 较典 典型 型的 的场 场景 遇到一 到一个 一个比 个比较 比较典 较典型 典型的 型的场 的场景 遇到一个 到一个比 一个比较 个比较典 比较典型 较典型的 典型的场 型的场景 登录 录接 接口 口没 没有 有出 出现 现明 明显 显漏 漏洞 洞利 利用 用痕 痕迹 登录接 录接口 接口没 口没有 没有出 有出现 出现明 现明显 明显漏 显漏洞 漏洞利 洞利用 利用痕 用痕迹 登录接口 录接口没 接口没有 口没有出 没有出现 有出现明 出现明显 现明显漏 明显漏洞 显漏洞利 漏洞利用 洞利用痕 利用痕迹 但日 日志 志里 里能 能看 看到 到大 大量 量异 异常 常请 请求 但日志 日志里 志里能 里能看 能看到 看到大 到大量 大量异 量异常 异常请 常请求 但日志里 日志里能 志里能看 里能看到 能看到大 看到大量 到大量异 大量异常 量异常请 异常请求 包括 括高 高频 频失 失败 败登 包括高 括高频 高频失 频失败 失败登 败登录 包括高频 括高频失 高频失败 频失败登 失败登录 参数 数结 结构 构异 参数结 数结构 结构异 构异常 参数结构 数结构异 结构异常 不同 的批 批量 量访 访问 的批量 批量访 量访问 的批量访 批量访问 以及 及少 少量 量疑 疑似 似探 探测 测请 以及少 及少量 少量疑 量疑似 疑似探 似探测 探测请 测请求 以及少量 及少量疑 少量疑似 量疑似探 疑似探测 似探测请 探测请求 这类 类问 问题 题在 在实 实际 际环 环境 境中 中很 很常 常见 这类问 类问题 问题在 题在实 在实际 实际环 际环境 环境中 境中很 中很常 很常见 这类问题 类问题在 问题在实 题在实际 在实际环 实际环境 际环境中 环境中很 境中很常 中很常见 它不 不一 一定 定意 意味 味着 着系 统已 已经 经被 被入 入侵 它不一 不一定 一定意 定意味 意味着 味着系 着系统 系统已 统已经 已经被 经被入 被入侵 它不一定 不一定意 一定意味 定意味着 意味着系 味着系统 着系统已 系统已经 统已经被 已经被入 经被入侵 但通 通常 常说 说明 明接 口暴 暴露 露面 但通常 通常说 常说明 说明接 明接口 接口暴 口暴露 暴露面 但通常说 通常说明 常说明接 说明接口 明接口暴 接口暴露 口暴露面 认证 证策 策略 认证策 证策略 认证策略 志可 可观 观测 测性 性和 和风 风控 控策 略存 存在 在短 短板 日志可 志可观 可观测 观测性 测性和 性和风 和风控 风控策 控策略 策略存 略存在 存在短 在短板 日志可观 志可观测 可观测性 观测性和 测性和风 性和风控 和风控策 风控策略 控策略存 策略存在 略存在短 存在短板 本文 文不 不讨 讨论 论任 任何 何攻 攻击 击利 用方 方法 本文不 文不讨 不讨论 讨论任 论任何 任何攻 何攻击 攻击利 击利用 利用方 用方法 本文不讨 文不讨论 不讨论任 讨论任何 论任何攻 任何攻击 何攻击利 攻击利用 击利用方 利用方法 重点 点放 放在 在如 如何 何从 从防 防守 守视 视角 角复 重点放 点放在 放在如 在如何 如何从 何从防 从防守 防守视 守视角 视角复 角复盘 重点放在 点放在如 放在如何 在如何从 如何从防 何从防守 从防守视 防守视角 守视角复 视角复盘 分析 析和 和加 加固 分析和 析和加 和加固 分析和加 析和加固 题场 景业 统采 采用 用常 见的 的前 前后 后端 端分 分离 离架 架构 问题场 题场景 场景业 景业务 系统采 统采用 采用常 用常见 常见的 见的前 的前后 前后端 后端分 端分离 分离架 离架构 问题场景 题场景业 场景业务 景业务系 务系统采 系统采用 统采用常 采用常见 用常见的 常见的前 见的前后 的前后端 前后端分 后端分离 端分离架 分离架构 口大 大致 致如 如下 接口大 口大致 大致如 致如下 录接口大 接口大致 口大致如 大致如下 排查 查时 时发 发现 现几 几个 个现 现象 排查时 查时发 时发现 发现几 现几个 几个现 个现象 排查时发 查时发现 时发现几 发现几个 现几个现
  16. SSRF(Server-Side Request Forgery,服务端请求伪造)并不只是“让服务器代替用户发起一次请求”。在真实系统中,它往往与网络分区、云环境元数据服务、内部管理接口以及权限模型相互叠加,最终形成从外部输入到内部资源的访问链路。对这类问题进行审计时,单纯搜索 requests.get() 或 HttpClient 并不够,更重要的是理解数据流、请求边界和部署环境。 一、问题背景与常见场景SSRF 通常出现在以下功能中: 根据用户提交的 URL 抓取远程图片、网页或 RSS。Webhook、回调地址、第三方通知地址配置。在线预览、文档转换、PDF 生成和远程资源解析。服务端健康检查、连通性测试和网络探测功能。代理下载、跳转跟随和文件导入功能。这些功能本身并不一定有问题。风险主要取决于三个因素:用户是否能够影响目标地址,服务端是否会访问不应暴露的网络区域,以及返回内容是否会被直接回显或进一步处理。 审计时应把 SSRF 看作一条完整的数据流,而不是某一行危险函数。输入校验、DNS 解析、连接建立、重定向、响应读取和后续解析,每一个环节都可能改变最终风险。 二、先画出请求链路,再判断风险一个典型的远程资源获取流程可以抽象为: 用户输入 URL ↓ 参数校验与规范化 ↓ DNS 解析 ↓ 建立 TCP/TLS 连接 ↓ 处理重定向 ↓ 读取响应 ↓ 解析或存储内容代码审计时建议沿着这条链路逐项确认: 输入是否允许完整 URL,还是只允许固定域名下的路径。校验发生在 DNS 解析之前还是之后。校验通过后是否重新解析 DNS。HTTP 客户端是否默认跟随重定向。是否允许非 HTTP、HTTPS 协议。是否限制响应大小、连接超时和读取超时。响应是否会被当作 XML、图片、压缩包或模板继续处理。如果只在入口处做字符串判断,例如判断 URL 是否包含某个域名,通常不足以证明安全。URL 解析存在用户名、端口、编码、IPv6、重定向和 DNS 变化等多个边界条件,字符串匹配很容易与实际连接目标不一致。 三、一个常见但不可靠的实现下面的示例仅用于说明审计思路。它试图通过字符串判断阻止访问内部地址,但实际防护能力很弱: public byte[] fetch(String input) throws IOException { if (input.contains("127.0.0.1") || input.contains("localhost")) { throw new IllegalArgumentException("blocked"); } URL url = new URL(input); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(3000); conn.setReadTimeout(5000); return conn.getInputStream().readAllBytes(); }这段代码至少存在以下问题: 通过 IP 的其他表达形式可能绕过文本判断。DNS 解析结果没有进行地址分类。没有限制重定向,目标服务器可能返回另一个地址。未限制响应体大小,容易造成内存或磁盘资源耗尽。允许任意协议转换或 URL 解析行为,具体风险取决于客户端实现。没有明确限制端口和出口网络范围。四、正确的审计方法:验证“最终连接目标”防护的核心不是判断用户输入长什么样,而是确认客户端最终连接到什么地址。建议把目标解析为结构化对象,并对主机、端口、协议和解析结果分别检查。 在 Java 服务中,可以将校验逻辑拆成以下几个阶段: URI uri = URI.create(input); String scheme = uri.getScheme(); String host = uri.getHost(); int port = uri.getPort(); if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) { throw new IllegalArgumentException("unsupported scheme"); } if (host == null || host.isBlank()) { throw new IllegalArgumentException("invalid host"); } if (port != -1 && port != 80 && port != 443) { throw new IllegalArgumentException("unsupported port"); }这只是基础校验,不能单独作为完整防护。之后还需要解析主机名,并对每个解析结果进行地址分类。对于业务不需要访问内网资源的服务,通常应拒绝回环地址、链路本地地址、私有地址、未指定地址以及其他保留地址。 InetAddress[] addresses = InetAddress.getAllByName(host); for (InetAddress address : addresses) { if (address.isAnyLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) { throw new IllegalArgumentException("destination is not allowed"); } }实际项目中不建议仅依赖这几个便捷方法。应结合组织的网络策略,对 IPv4、IPv6、云平台地址段、办公网地址段和容器网络地址段进行统一封装,并为该组件配套单元测试和回归测试。 五、DNS 重绑定与校验时机问题一个经常被忽略的问题是:校验阶段解析到的地址,不一定是连接阶段使用的地址。若应用先解析域名并判断结果安全,随后由 HTTP 客户端再次解析域名,就存在两次解析结果不一致的可能。 因此,较稳妥的做法包括: 在受控网络环境中使用固定的出口代理,由代理统一执行最终连接和地址策略。使用能够固定解析结果或提供自定义 DNS 解析器的 HTTP 客户端。在建立连接前后都执行地址检查,并避免客户端自动重新解析未经校验的主机名。通过网络层策略阻断应用容器访问不必要的内网网段,不能只依赖业务代码。在审计报告中,建议明确写出“校验点”和“连接点”是否一致,而不是笼统描述为“存在 DNS 绕过”。这样更便于开发人员定位客户端配置和网络出口问题。 六、重定向是第二个高频风险点即使初始 URL 通过了校验,服务端也可能返回 3xx 响应,引导客户端访问另一个地址。如果 HTTP 客户端默认跟随重定向,那么真正的访问目标已经发生变化。 安全处理方式有两种: 业务不需要重定向时,直接关闭自动跟随,并将 3xx 作为失败处理。业务确实需要重定向时,对每一跳的目标重新解析、重新校验,并限制最大跳转次数。HttpClient client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(3)) .build();对于下载器、图片代理等功能,建议把重定向次数限制在较小范围内,并限制每次跳转只能使用允许的协议和端口。不要仅校验初始地址。 七、响应内容同样属于攻击面SSRF 的影响不只来自“能否访问某个地址”,还取决于应用如何处理响应。常见的二次风险包括: 读取内部接口返回的配置、调试信息或身份凭据。将响应内容直接回显给前端,形成信息泄露。把响应当作 XML 解析,引入 XXE 风险。把响应当作图片、压缩包或文档处理,触发解析器漏洞。无限读取大响应,导致内存、磁盘或线程资源耗尽。因此,远程资源获取功能至少应设置连接超时、读取超时、最大响应大小、最大重定向次数和允许的 Content-Type。不要只根据响应头判断安全,服务端返回的头部本身也可能不可信,必要时应对实际内容做有限度的格式识别。 long maxBytes = 5 * 1024 * 1024; try (InputStream in = response.body()) { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; long 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); } }八、在隔离环境中进行安全验证验证 SSRF 时不应直接探测生产内网,也不应访问真实的管理接口或云平台元数据服务。更推荐建立最小化实验环境: 使用本地容器启动一个业务服务和一个仅用于记录请求的测试服务。让测试服务返回固定的非敏感标记,例如 TEST-SSRF-OK。通过应用日志、容器网络日志和代理日志确认实际请求目标。对重定向、超时、超大响应和不同地址类型分别编写自动化测试。例如,可以在本地测试服务中记录请求来源、Host、路径和 User-Agent,再由业务服务访问该测试地址。测试的重点是确认请求是否按照预期被限制,而不是尝试读取任何真实敏感信息。 curl --max-time 3 SSRF,合理的身份权限也能降低进一步影响。 十、代码审计中的排查清单搜索所有由用户输入决定目标地址的代码路径,而不只是搜索 HTTP 客户端调用。检查 URL 是否经过结构化解析,避免纯字符串黑名单。确认是否校验协议、主机、端口和解析后的全部地址。检查 IPv4、IPv6、域名多解析结果以及重定向处理。确认 DNS 校验与实际连接是否使用同一套解析结果。检查连接、读取、响应大小和重定向次数限制。确认响应是否进入 XML、图片、压缩包、模板或脚本解析流程。检查日志中是否包含完整 URL、Authorization 头或敏感响应内容。结合容器、主机、防火墙和云网络策略判断真实可达范围。为修复方案补充回归测试,避免后续重构重新引入问题。十一、修复后的回归测试设计回归测试不应只验证一个被拦截的地址,而应覆盖整个请求状态机。可以至少准备以下测试类别: 合法的业务域名和合法端口能够正常访问。回环、私有、链路本地和未指定地址被拒绝。主机名解析得到多个地址时,只要存在不允许地址就拒绝。初始地址合法、重定向目标不合法时,请求被终止。响应超过大小限制时,连接被安全关闭。连接超时和读取超时不会长期占用工作线程。不支持的协议和端口无法进入底层客户端。错误日志包含足够的排查信息,但不泄露敏感响应内容。总结SSRF 审计的关键不是寻找某个“万能黑名单”,而是建立清晰的请求边界:谁提供目标地址,应用解析出了什么,最终连接到了哪里,响应又被如何处理。比较可靠的治理方式通常由四部分组成:结构化解析与严格校验、对 DNS 和重定向保持一致控制、限制响应处理资源,以及通过代理和网络隔离补充应用层防护。 从工程实践看,最值得优先改进的往往不是某一行过滤代码,而是把远程请求能力集中到一个经过审计的组件中,统一处理协议、地址、端口、超时、大小和日志策略。这样既能降低修复成本,也能避免不同业务各自实现一套不完整的 SSRF 防护逻辑。 服务器、云资产与权限边界梳理示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不只 只是 并不只 不只是 并不只是 让服 务器 器代 代替 替用 用户 户发 发起 起一 一次 次请 让服务 服务器 务器代 器代替 代替用 替用户 用户发 户发起 发起一 起一次 一次请 次请求 让服务器 服务器代 务器代替 器代替用 代替用户 替用户发 用户发起 户发起一 发起一次 起一次请 一次请求 在真 真实 实系 系统 统中 在真实 真实系 实系统 系统中 在真实系 真实系统 实系统中 它往 往往 往与 与网 网络 络分 分区 它往往 往往与 往与网 与网络 网络分 络分区 它往往与 往往与网 往与网络 与网络分 网络分区 云环 环境 境元 元数 数据 据服 云环境 环境元 境元数 元数据 数据服 据服务 云环境元 环境元数 境元数据 元数据服 数据服务 内部 部管 管理 理接 接口 口以 以及 及权 权限 限模 模型 型相 相互 互叠 叠加 内部管 部管理 管理接 理接口 接口以 口以及 以及权 及权限 权限模 限模型 模型相 型相互 相互叠 互叠加 内部管理 部管理接 管理接口 理接口以 接口以及 口以及权 以及权限 及权限模 权限模型 限模型相 模型相互 型相互叠 相互叠加 最终 终形 形成 成从 从外 外部 部输 输入 入到 到内 部资 资源 源的 的访 访问 问链 链路 最终形 终形成 形成从 成从外 从外部 外部输 部输入 输入到 入到内 到内部 内部资 部资源 资源的 源的访 的访问 访问链 问链路 最终形成 终形成从 形成从外 成从外部 从外部输 外部输入 部输入到 输入到内 入到内部 到内部资 内部资源 部资源的 资源的访 源的访问 的访问链 访问链路 对这 这类 类问 问题 题进 进行 行审 审计 计时 对这类 这类问 类问题 问题进 题进行 进行审 行审计 审计时 对这类问 这类问题 类问题进 问题进行 题进行审 进行审计 行审计时 单纯 纯搜 搜索 单纯搜 纯搜索 单纯搜索 不够 并不够 更重 重要 要的 的是 是理 理解 解数 据流 更重要 重要的 要的是 的是理 是理解 理解数 解数据 数据流 更重要的 重要的是 要的是理 的是理解 是理解数 理解数据 解数据流 求边 边界 界和 和部 部署 署环 请求边 求边界 边界和 界和部 和部署 部署环 署环境 请求边界 求边界和 边界和部 界和部署 和部署环 部署环境 题背 背景 景与 与常 常见 见场 场景 问题背 题背景 背景与 景与常 与常见 常见场 见场景 问题背景 题背景与 背景与常 景与常见 与常见场 常见场景 通常 常出 出现 现在 在以 以下 下功 功能 能中 通常出 常出现 出现在 现在以 在以下 以下功 下功能 功能中 通常出现 常出现在 出现在以 现在以下 在以下功 以下功能 下功能中 根据 据用 户提 提交 交的 根据用 据用户 用户提 户提交 提交的 根据用户 据用户提 用户提交 户提交的 抓取 取远 远程 程图 图片 抓取远 取远程 远程图 程图片 抓取远程 取远程图 远程图片 网页 页或 网页或 回调 调地 地址 回调地 调地址 回调地址 第三 三方 方通 通知 知地 址配 配置 第三方 三方通 方通知 通知地 知地址 地址配 址配置 第三方通 三方通知 方通知地 通知地址 知地址配 地址配置 在线 线预 预览 在线预 线预览 在线预览 文档 档转 转换 文档转 档转换 文档转换 生成 成和 和远 程资 源解 解析 生成和 成和远 和远程 远程资 程资源 资源解 源解析 生成和远 成和远程 和远程资 远程资源 程资源解 资源解析 端健 健康 康检 检查 务端健 端健康 健康检 康检查 服务端健 务端健康 端健康检 健康检查 连通 通性 性测 测试 试和 和网 络探 探测 测功 连通性 通性测 性测试 测试和 试和网 和网络 网络探 络探测 探测功 测功能 连通性测 通性测试 性测试和 测试和网 试和网络 和网络探 网络探测 络探测功 探测功能 代理 理下 下载 代理下 理下载 代理下载 跳转 转跟 跟随 随和 和文 文件 件导 导入 入功 跳转跟 转跟随 跟随和 随和文 和文件 文件导 件导入 导入功 入功能 跳转跟随 转跟随和 跟随和文 随和文件 和文件导 文件导入 件导入功 导入功能 这些 些功 能本 本身 身并 不一 一定 定有 有问 这些功 些功能 功能本 能本身 本身并 身并不 并不一 不一定 一定有 定有问 有问题 这些功能 些功能本 功能本身 能本身并 本身并不 身并不一 并不一定 不一定有 一定有问 定有问题
  17. 背景文件上传是 Web 系统里很常见的功能:头像、附件、工单截图、合同文档、插件包导入等都会涉及。也正因为它离用户输入很近,一旦校验和存储策略处理不好,风险往往不是单点问题,而是会串到解析、访问控制、存储桶权限、静态资源服务和后端任务处理链路。 这篇文章整理的是我在日常代码审计和漏洞复现中比较常用的一套检查方法,重点放在“如何识别风险”和“如何把风险收敛掉”。不讨论非法利用,也不提供面向攻击的操作路径,更多是给开发、安全同学做自查时参考。 典型场景某业务系统提供附件上传能力,后端使用 Spring Boot,文件最终落到本地磁盘或对象存储,数据库中保存文件名、访问路径、上传人、业务类型等信息。常见代码大概长这样: @PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { String originalName = file.getOriginalFilename(); String path = uploadDir + "/" + originalName; file.transferTo(new File(path)); return Result.ok("/files/" + originalName); }这类代码看起来能跑,但安全边界非常薄。审计时一般不会只看“能不能上传某类危险文件”,而是会沿着完整链路看:文件名如何处理、类型如何识别、保存位置是否可执行、访问 URL 是否受控、是否存在覆盖、是否可能被后端解析、是否有异步任务处理文件内容。 问题拆解文件上传相关风险可以拆成几个层面: 文件名风险:目录穿越、特殊字符、同名覆盖、日志污染、前端展示 XSS。类型校验风险:只检查后缀、只检查 Content-Type、黑名单不完整、双后缀误判。存储风险:上传目录位于 Web 可执行路径下,静态资源服务器直接暴露原始文件。解析风险:图片处理、压缩包解压、Office/PDF 解析、病毒扫描组件本身可能触发漏洞。权限风险:附件 URL 可枚举,跨用户访问,临时文件长期有效。资源消耗风险:超大文件、压缩炸弹、海量小文件、慢上传拖垮连接。很多事故不是某一个校验没写,而是多个弱点叠加。例如:后缀校验通过、文件存储在静态目录、访问路径可预测、没有鉴权,最后导致任意用户都能访问他人附件。 代码审计关注点1. 原始文件名不应直接参与存储路径审计时首先搜索 MultipartFile、upload、transferTo、FileOutputStream、Files.copy 等关键点。重点看 originalFilename 是否被直接拼接到路径中。 // 风险写法:直接使用用户提交的文件名 String originalName = file.getOriginalFilename(); File target = new File(uploadDir + "/" + originalName); file.transferTo(target);推荐做法是:业务展示名和真实存储名分离。真实存储名由服务端生成,展示名只作为普通字段保存,并在展示时做 HTML 转义。 String originalName = StringUtils.cleanPath(file.getOriginalFilename()); String ext = getSafeExtension(originalName); String storageName = UUID.randomUUID().toString().replace("-", "") + ext; Path target = Paths.get(uploadDir).resolve(storageName).normalize(); if (!target.startsWith(Paths.get(uploadDir).normalize())) { throw new IllegalArgumentException("invalid path"); } Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);这里有两个点比较关键:一是 normalize 后检查路径是否仍在上传目录内,二是不要让用户决定真实落盘文件名。 2. 类型校验要使用白名单,而不是黑名单黑名单很容易漏,比如不同语言、不同服务器、不同中间件对扩展名和解析规则的理解不完全一致。更稳妥的是按业务要求设置白名单,比如头像只允许 jpg、png、webp,合同只允许 pdf、docx。 private static final Set<String> ALLOWED_EXT = Set.of(".jpg", ".jpeg", ".png", ".webp", ".pdf"); private String getSafeExtension(String filename) { String lower = filename.toLowerCase(Locale.ROOT); int index = lower.lastIndexOf('.'); if (index < 0) { throw new IllegalArgumentException("missing extension"); } String ext = lower.substring(index); if (!ALLOWED_EXT.contains(ext)) { throw new IllegalArgumentException("unsupported file type"); } return ext; }不过只看后缀仍然不够。Content-Type 也不能完全信任,因为它来自客户端请求头。更合适的是做多层判断:扩展名白名单、服务端 MIME 探测、必要时检查文件魔数。 String detected = Files.probeContentType(target); if (detected == null || !detected.startsWith("image/")) { Files.deleteIfExists(target); throw new IllegalArgumentException("invalid image content"); }需要注意,Files.probeContentType 在不同系统上的行为可能不同,不能作为唯一依据。对于图片类上传,通常还会使用 ImageIO 或专门库读取一次,确认确实能被安全解析。 3. 上传目录不要具备执行能力比较稳的策略是:上传文件不放在应用部署目录下,更不要放在能被脚本引擎执行的位置。对外访问时通过下载接口或静态资源服务读取,统一加鉴权、限速和响应头控制。 # Nginx 示例:上传目录只作为静态文件读取,不启用任何脚本解析 location /uploads/ { alias /data/app/uploads/; autoindex off; default_type application/octet-stream; add_header X-Content-Type-Options nosniff; add_header Content-Disposition "attachment"; }如果业务需要图片在线预览,可以按类型单独处理,不建议所有文件都 inline 展示。对于用户可控内容,响应头里加 X-Content-Type-Options: nosniff 是一个低成本但很有效的防线。 4. 访问控制不能依赖“路径难猜”很多系统会把文件名改成 UUID,然后认为足够安全。UUID 可以降低枚举概率,但不等于权限控制。附件和业务对象通常存在归属关系,下载时应该检查当前用户是否有访问该业务对象的权限。 @GetMapping("/files/{id}") public ResponseEntity<Resource> download(@PathVariable Long id, Principal principal) { FileMeta meta = fileService.getMeta(id); if (!permissionService.canRead(principal.getName(), meta.getBizId())) { return ResponseEntity.status(403).build(); } Resource resource = fileStorage.load(meta.getStorageName()); return ResponseEntity.ok() .header("X-Content-Type-Options", "nosniff") .header("Content-Disposition", "attachment; filename=\"" + meta.getSafeDownloadName() + "\"") .body(resource); }这里建议接口使用文件 ID,而不是直接暴露真实存储路径。真实路径、对象存储 key、bucket 名称等都不应成为前端可控参数。 5. 解压和解析类功能要单独建模上传压缩包、导入 Excel、解析 PDF 这类功能风险更高。它们不只是“保存文件”,还会触发后端解析逻辑。审计时要重点看: 解压是否限制目标目录,防止路径逃逸。是否限制解压后文件数量、总大小、目录层级。是否限制单个文件大小和压缩比。解析库版本是否存在已知漏洞。解析过程是否放在低权限、隔离环境中。解压时至少要做 canonical path 检查,确保每个解压出的文件都在目标目录内。 Path destDir = Paths.get("/data/import/workdir").toRealPath(); Path out = destDir.resolve(entry.getName()).normalize(); if (!out.startsWith(destDir)) { throw new SecurityException("zip entry path traversal detected"); }同时要限制解压规模,避免被异常压缩包拖垮磁盘或 CPU。这个点在内网系统里很容易被忽略。 关键加固清单检查项建议做法文件名服务端生成存储名,原始名仅用于展示并转义类型校验扩展名白名单 + MIME 探测 + 必要的内容识别存储位置放在非 Web 执行目录,避免脚本解析访问控制下载接口绑定业务权限,不依赖路径不可猜响应头设置 nosniff,按需设置 attachment资源限制限制单文件大小、总量、频率、压缩包解压规模对象存储默认私有桶,通过后端签发短期访问地址日志审计记录上传人、业务 ID、文件摘要、大小、类型、处理结果Spring Boot 中的基础限制框架层可以先做一层基础限制,避免超大请求直接打到业务逻辑。示例配置如下: spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=20MB server.tomcat.max-swallow-size=20MB这只是第一层保护,不能替代业务层的大小、类型、权限校验。特别是多个文件批量上传时,还要限制文件数量和用户级频率。 审计时的排查路径实际看项目时,我一般按下面顺序走,效率会高一些: 全局搜索上传入口:MultipartFile、RequestPart、transferTo、Files.copy、FileOutputStream。确认文件元数据表:看存储名、原始名、业务 ID、用户 ID、访问路径如何设计。追踪下载接口:看是否根据业务权限鉴权,是否存在按 path/key 直接读取。检查静态资源配置:Nginx、Spring ResourceHandler、对象存储桶策略。检查异步处理:缩略图、OCR、解压、格式转换、病毒扫描等任务。检查异常处理:失败文件是否残留,临时目录是否定期清理。如果只能做快速基线,优先看三件事:是否直接使用原始文件名、上传目录是否可执行、下载是否缺权限校验。这三类问题命中率较高,影响也比较直接。 一些容易被忽略的细节前端限制不算安全控制:accept 属性、JS 校验只能提升体验,不能作为后端信任依据。对象存储公有读要谨慎:公有读一旦配错,历史附件可能被批量访问。更建议私有桶 + 短期签名 URL。文件摘要有助于审计:保存 SHA-256 可以辅助去重、追踪异常文件和排查事件。临时目录也要管:很多系统只清理正式文件,不清理导入失败、解析失败的临时文件。错误信息不要暴露路径:异常返回里不要带服务器绝对路径、对象存储 key、内部桶名。总结文件上传安全不是单个 if 判断能解决的问题。比较稳的思路是把它当成一条链路来治理:入口限制、类型识别、隔离存储、受控访问、解析防护、日志审计和定期清理都要覆盖。 从代码审计角度看,最值得优先修的是三点:不要直接使用用户文件名落盘,不要把上传目录放到可执行路径下,不要让附件访问绕过业务权限。把这几处收敛好,绝大多数常见上传风险都能降到可控范围。 经验上,上传功能越“通用”,越需要边界清晰。不要为了复用一个上传接口,让头像、合同、导入包、插件包走同一套弱校验逻辑。按业务类型拆策略,反而更容易长期维护。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 系统 统里 里很 很常 常见 见的 的功 功能 系统里 统里很 里很常 很常见 常见的 见的功 的功能 系统里很 统里很常 里很常见 很常见的 常见的功 见的功能 头像 附件 工单 单截 截图 工单截 单截图 工单截图 合同 同文 文档 合同文 同文档 合同文档 插件 件包 包导 导入 入等 等都 都会 会涉 涉及 插件包 件包导 包导入 导入等 入等都 等都会 都会涉 会涉及 插件包导 件包导入 包导入等 导入等都 入等都会 等都会涉 都会涉及 也正 正因 因为 为它 它离 离用 用户 户输 输入 入很 很近 也正因 正因为 因为它 为它离 它离用 离用户 用户输 户输入 输入很 入很近 也正因为 正因为它 因为它离 为它离用 它离用户 离用户输 用户输入 户输入很 输入很近 一旦 旦校 校验 验和 和存 存储 储策 策略 略处 处理 理不 不好 一旦校 旦校验 校验和 验和存 和存储 存储策 储策略 策略处 略处理 处理不 理不好 一旦校验 旦校验和 校验和存 验和存储 和存储策 存储策略 储策略处 策略处理 略处理不 处理不好 风险 险往 往往 往不 不是 是单 单点 点问 问题 风险往 险往往 往往不 往不是 不是单 是单点 单点问 点问题 风险往往 险往往不 往往不是 往不是单 不是单点 是单点问 单点问题 而是 是会 会串 串到 到解 解析 而是会 是会串 会串到 串到解 到解析 而是会串 是会串到 会串到解 串到解析 访问 问控 控制 访问控 问控制 访问控制 储桶 桶权 权限 存储桶 储桶权 桶权限 存储桶权 储桶权限 静态 态资 资源 源服 服务 务和 和后 后端 端任 任务 务处 理链 链路 静态资 态资源 资源服 源服务 服务和 务和后 和后端 后端任 端任务 任务处 务处理 处理链 理链路 静态资源 态资源服 资源服务 源服务和 服务和后 务和后端 和后端任 后端任务 端任务处 任务处理 务处理链 处理链路 这篇 篇文 文章 章整 整理 理的 的是 是我 我在 在日 日常 常代 代码 码审 审计 计和 和漏 漏洞 洞复 复现 现中 中比 比较 较常 常用 用的 的一 一套 套检 检查 查方 方法 这篇文 篇文章 文章整 章整理 整理的 理的是 的是我 是我在 我在日 在日常 日常代 常代码 代码审 码审计 审计和 计和漏 和漏洞 漏洞复 洞复现 复现中 现中比 中比较 比较常 较常用 常用的 用的一 的一套 一套检 套检查 检查方 查方法 这篇文章 篇文章整 文章整理 章整理的 整理的是 理的是我 的是我在 是我在日 我在日常 在日常代 日常代码 常代码审 代码审计 码审计和 审计和漏 计和漏洞 和漏洞复 漏洞复现 洞复现中 复现中比 现中比较 中比较常 比较常用 较常用的 常用的一 用的一套 的一套检 一套检查 套检查方 检查方法 重点 点放 放在 重点放 点放在 重点放在 如何 何识 识别 别风 如何识 何识别 识别风 别风险 如何识别 何识别风 识别风险 何把 把风 险收 收敛 敛掉 如何把 何把风 把风险 风险收 险收敛 收敛掉 如何把风 何把风险 把风险收 风险收敛 险收敛掉 不讨 讨论 论非 非法 法利 利用 不讨论 讨论非 论非法 非法利 法利用 不讨论非 讨论非法 论非法利 非法利用 也不 不提 提供 供面 面向 向攻 攻击 击的 的操 操作 作路 路径 也不提 不提供 提供面 供面向 面向攻 向攻击 攻击的 击的操 的操作 操作路 作路径 也不提供 不提供面 提供面向 供面向攻 面向攻击 向攻击的 攻击的操 击的操作 的操作路 操作路径 更多 多是 是给 给开 开发 更多是 多是给 是给开 给开发 更多是给 多是给开 是给开发 安全 全同 同学 学做 做自 自查 查时 时参 参考 安全同 全同学 同学做 学做自 做自查 自查时 查时参 时参考 安全同学 全同学做 同学做自 学做自查 做自查时 自查时参 查时参考 典型 型场 场景 景某 某业 业务 务系 统提 供附 传能 能力 典型场 型场景 场景某 景某业 某业务 业务系 务系统 系统提 统提供 提供附 供附件 附件上 上传能 传能力 典型场景 型场景某 场景某业 景某业务 某业务系 业务系统 务系统提 系统提供 统提供附 提供附件 供附件上 附件上传 件上传能 上传能力 端使 使用 后端使 端使用 后端使用 件最 最终 终落 落到 到本 本地 地磁 磁盘 盘或 或对 对象 象存 文件最 件最终 最终落 终落到 落到本 到本地
  18. 背景最近在做内部系统安全复盘时,遇到一个比较典型的问题:某业务后台已经接入统一登录,但部分管理接口仍然可以在未登录状态下被访问。问题本身不复杂,却很有代表性,涉及路由鉴权、前后端权限边界、接口设计习惯以及代码审计方法。 这类问题在中小型后台、快速迭代的运营系统、历史项目迁移中比较常见。前端页面可能已经做了登录拦截,菜单也按角色隐藏,但后端接口如果没有统一鉴权,实际风险仍然存在。 本文只讨论防御视角下的漏洞分析和修复思路,不涉及针对真实目标的攻击利用过程。示例均为本地测试环境中的抽象代码。问题场景系统大致结构如下: 前端:Vue 单页应用,通过路由守卫判断是否存在 token。后端:Spring Boot 提供 REST API。登录:统一认证服务签发 JWT。权限:部分接口使用注解校验,部分接口依赖网关或前端控制。问题接口位于后台管理模块,例如用户列表、配置查询、导出任务状态等接口。页面层面无法直接进入,但接口在缺少登录态时仍返回数据。 典型表现包括: 访问管理页面会跳转登录页,但直接访问接口仍返回 200。同一 Controller 中部分方法有权限注解,部分方法没有。新增接口未纳入统一拦截器白名单/黑名单策略。测试环境配置被复制到生产环境,导致部分路径被放行。技术分析这类未授权访问通常不是单点代码失误,而是权限模型不清晰导致的工程问题。常见成因有以下几类。 1. 只在前端做登录拦截前端路由守卫只能提升用户体验,不能作为安全边界。比如常见的写法: router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } })这段逻辑只能阻止普通用户通过页面导航访问后台,无法约束后端 API。只要后端没有校验,接口仍然是暴露的。 2. 后端鉴权策略不统一在 Spring Boot 项目中,经常看到两种混用方式: 部分接口通过拦截器校验 token。部分接口通过注解如 @PreAuthorize 控制权限。少量历史接口完全没有接入权限体系。例如: @RestController @RequestMapping("/admin/user") public class AdminUserController { @PreAuthorize("hasAuthority('admin:user:list')") @GetMapping("/list") public List<UserVO> list() { return userService.listUsers(); } @GetMapping("/detail") public UserVO detail(Long id) { return userService.getUserDetail(id); } }上面 /list 有权限校验,但 /detail 没有。如果项目没有全局认证拦截,/detail 就可能成为未授权访问点。 3. 白名单配置过宽另一个常见问题是认证拦截器或网关白名单配置过于宽泛: security: ignore-paths: - /login - /captcha - /actuator/** - /admin/**/export - /admin/config/**配置初衷可能是放行登录、验证码、健康检查,或者临时调试导出接口。但随着业务迭代,某些路径下新增了敏感接口,白名单规则没有同步收敛,最终导致过度放行。 尤其需要注意通配符规则,例如 /admin/**、/**/export、/api/public/**。这类配置短期方便,长期风险较高。 4. 网关鉴权和服务鉴权边界不清有些系统将认证逻辑放在 API Gateway 或 Nginx 层,后端服务默认信任来自内网的请求。这种架构不是不能用,但必须保证: 后端服务不能被外部直接访问。网关转发时要传递可信身份信息。服务端仍需对关键操作做权限校验。内部调用也要有明确的认证边界。如果后端服务端口误暴露,或者测试环境与生产环境网络策略不一致,就容易出现绕过网关直接访问服务接口的问题。 代码审计关键步骤针对这类问题,建议不要只靠手工点页面,而是结合路由、配置、注解和日志做系统性排查。 1. 梳理接口清单优先从后端路由入手,导出所有 Controller 映射关系。Spring Boot 可以通过 Actuator 或启动日志辅助查看,生产环境不建议直接开放相关端点。 # 本地测试环境查看接口映射 curl Actuator,也可以用 IDE 搜索: @RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping重点关注路径中包含以下关键词的接口: admin、manage、systemuser、role、permissionconfig、settingexport、downloadlog、audit2. 对比鉴权注解覆盖情况可以用简单脚本或 IDE 结构视图检查 Controller 方法是否缺少鉴权注解。不是所有方法都必须写注解,但管理类接口至少应有统一认证兜底。 常见需要检查的注解包括: @PreAuthorize@Secured@RolesAllowed项目自定义注解,例如 @RequiresPermission、@AuthCheck一个相对稳妥的做法是:全局先校验登录态,再由方法级注解校验具体权限。也就是认证和授权分层处理。 3. 检查拦截器与白名单重点看 WebMvcConfigurer、Filter、SecurityConfig、Gateway Filter 等位置。 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/login", "/captcha", "/health" ); } }这里的原则是:默认拦截,明确放行。放行路径越少越好,且需要定期审查。 不建议使用下面这种配置: .excludePathPatterns( "/admin/**", "/api/**/public/**", "/**/download/**" );如果确实存在公开下载或公开查询接口,建议单独放在明确的 public Controller 下,避免与后台管理接口混用路径。 4. 验证不同身份下的访问结果测试时至少准备三类请求: 无 token 请求。普通用户 token 请求。管理员 token 请求。预期结果应当清晰: 请求身份后台列表接口个人信息接口公开验证码接口无 token401401200普通用户403200200管理员200200200这里要区分 401 和 403: 401 Unauthorized:未认证,缺少有效身份。403 Forbidden:已认证,但权限不足。很多系统统一返回 200 再用业务码表示失败,这种方式不是完全不可行,但会增加网关、监控和自动化测试的判断成本。新项目建议尽量使用标准 HTTP 状态码。 修复建议1. 建立默认拒绝策略认证策略建议采用默认拒绝,而不是默认放行。除登录、验证码、静态资源、健康检查等明确公开接口外,其余接口都必须先通过认证。 http .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/captcha", "/health").permitAll() .anyRequest().authenticated() );如果项目使用 Spring Security,这是比较清晰的基线。对于自研拦截器,也应保持类似逻辑。 2. 关键接口增加方法级授权只校验登录态还不够,后台管理接口应继续校验角色或权限点。 @PreAuthorize("hasAuthority('system:user:read')") @GetMapping("/admin/user/detail") public UserVO detail(@RequestParam Long id) { return userService.getUserDetail(id); }如果权限点过细,也要避免走向另一个极端:权限命名混乱、无人维护。建议按资源和动作组织,例如: system:user:readsystem:user:updatesystem:role:assignaudit:log:export3. 对敏感数据做字段级控制未授权访问的危害往往来自返回数据过多。即使接口权限修好了,也建议检查返回对象,避免把内部字段直接暴露给前端。 例如用户详情接口不应返回: 密码哈希盐值重置密码 token内部备注身份证、手机号等非必要敏感字段推荐使用专门的 VO/DTO,不要直接返回数据库实体。 public class UserVO { private Long id; private String username; private String displayName; private String status; }4. 加入自动化安全回归这类问题很适合做自动化回归。可以在集成测试中覆盖关键接口的无 token、低权限、高权限访问结果。 @Test void adminApiShouldRejectAnonymousRequest() throws Exception { mockMvc.perform(get("/admin/user/list")) .andExpect(status().isUnauthorized()); }对历史项目而言,哪怕先覆盖核心管理接口,也比完全依赖人工测试更可靠。 5. 日志与告警权限拦截失败不是只返回错误就结束了,建议记录必要日志,便于排查异常访问。日志内容应包含: 请求路径请求方法用户 ID 或匿名标识客户端 IP失败原因:未登录、token 失效、权限不足注意不要在日志中记录完整 token、密码、验证码等敏感内容。 注意点不要把前端菜单隐藏当作权限控制。不要长期保留临时白名单。不要在多个地方重复实现互不一致的鉴权逻辑。不要让测试环境安全配置直接进入生产。不要默认信任内网请求,尤其是容器化和微服务环境。不要把实体对象直接作为接口返回值。一个较实用的排查清单检查项建议接口是否默认需要登录除明确公开接口外全部认证后台接口是否有权限点管理类接口增加方法级授权白名单是否过宽避免使用大范围通配符网关是否是唯一入口确认后端服务未直接暴露返回字段是否过多使用 DTO/VO 控制输出是否有自动化测试覆盖匿名、低权、高权访问是否有审计日志记录失败原因但避免敏感信息总结后台未授权访问通常不是某个接口少写一行代码这么简单,而是权限体系缺少统一边界的表现。修复时不要只补单个接口,更应回到整体设计:默认拒绝、统一认证、细粒度授权、最小化返回字段、自动化回归。 从代码审计角度看,排查这类问题最有效的方法是把接口清单、鉴权配置、权限注解和实际访问结果放在一起对照。只看页面很容易漏,只看代码也可能忽略运行时配置。对于长期维护的业务系统,这套检查方式值得沉淀到发布流程中。 网络请求、日志与边界流量分析示意 背景 景最 最近 近在 在做 做内 内部 部系 系统 统安 安全 全复 复盘 盘时 背景最 景最近 最近在 近在做 在做内 做内部 内部系 部系统 系统安 统安全 安全复 全复盘 复盘时 背景最近 景最近在 最近在做 近在做内 在做内部 做内部系 内部系统 部系统安 系统安全 统安全复 安全复盘 全复盘时 遇到 到一 一个 个比 比较 较典 典型 型的 的问 问题 遇到一 到一个 一个比 个比较 比较典 较典型 典型的 型的问 的问题 遇到一个 到一个比 一个比较 个比较典 比较典型 较典型的 典型的问 型的问题 某业 业务 务后 后台 台已 已经 经接 接入 入统 统一 一登 登录 某业务 业务后 务后台 后台已 台已经 已经接 经接入 接入统 入统一 统一登 一登录 某业务后 业务后台 务后台已 后台已经 台已经接 已经接入 经接入统 接入统一 入统一登 统一登录 但部 部分 分管 管理 理接 接口 口仍 仍然 然可 可以 以在 在未 未登 录状 状态 态下 下被 被访 访问 但部分 部分管 分管理 管理接 理接口 接口仍 口仍然 仍然可 然可以 可以在 以在未 在未登 未登录 登录状 录状态 状态下 态下被 下被访 被访问 但部分管 部分管理 分管理接 管理接口 理接口仍 接口仍然 口仍然可 仍然可以 然可以在 可以在未 以在未登 在未登录 未登录状 登录状态 录状态下 状态下被 态下被访 下被访问 题本 本身 身不 不复 复杂 问题本 题本身 本身不 身不复 不复杂 问题本身 题本身不 本身不复 身不复杂 却很 很有 有代 代表 表性 却很有 很有代 有代表 代表性 却很有代 很有代表 有代表性 涉及 及路 路由 由鉴 鉴权 涉及路 及路由 路由鉴 由鉴权 涉及路由 及路由鉴 路由鉴权 前后 后端 端权 权限 限边 边界 前后端 后端权 端权限 权限边 限边界 前后端权 后端权限 端权限边 权限边界 口设 设计 计习 习惯 惯以 以及 及代 代码 码审 审计 计方 方法 接口设 口设计 设计习 计习惯 习惯以 惯以及 以及代 及代码 代码审 码审计 审计方 计方法 接口设计 口设计习 设计习惯 计习惯以 习惯以及 惯以及代 以及代码 及代码审 代码审计 码审计方 审计方法 这类 类问 题在 在中 中小 小型 型后 这类问 类问题 问题在 题在中 在中小 中小型 小型后 型后台 这类问题 类问题在 问题在中 题在中小 在中小型 中小型后 小型后台 快速 速迭 迭代 代的 的运 运营 营系 快速迭 速迭代 迭代的 代的运 的运营 运营系 营系统 快速迭代 速迭代的 迭代的运 代的运营 的运营系 运营系统 历史 史项 项目 目迁 迁移 移中 中比 较常 常见 历史项 史项目 项目迁 目迁移 迁移中 移中比 中比较 比较常 较常见 历史项目 史项目迁 项目迁移 目迁移中 迁移中比 移中比较 中比较常 比较常见 前端 端页 页面 面可 可能 能已 经做 做了 了登 录拦 拦截 前端页 端页面 页面可 面可能 可能已 能已经 已经做 经做了 做了登 了登录 登录拦 录拦截 前端页面 端页面可 页面可能 面可能已 可能已经 能已经做 已经做了 经做了登 做了登录 了登录拦 登录拦截 菜单 单也 也按 按角 角色 色隐 隐藏 菜单也 单也按 也按角 按角色 角色隐 色隐藏 菜单也按 单也按角 也按角色 按角色隐 角色隐藏 但后 端接 口如 如果 果没 没有 有统 一鉴 但后端 后端接 端接口 接口如 口如果 如果没 果没有 没有统 有统一 统一鉴 一鉴权 但后端接 后端接口 端接口如 接口如果 口如果没 如果没有 果没有统 没有统一 有统一鉴 统一鉴权 实际 际风 风险 险仍 然存 存在 实际风 际风险 风险仍 险仍然 仍然存 然存在 实际风险 际风险仍 风险仍然 险仍然存 仍然存在 本文 文只 只讨 讨论 论防 防御 御视 视角 角下 下的 的漏 漏洞 洞分 分析 析和 和修 修复 复思 思路 本文只 文只讨 只讨论 讨论防 论防御 防御视 御视角 视角下 角下的 下的漏 的漏洞 漏洞分 洞分析 分析和 析和修 和修复 修复思 复思路 本文只讨 文只讨论 只讨论防 讨论防御 论防御视 防御视角 御视角下 视角下的 角下的漏 下的漏洞 的漏洞分 漏洞分析 洞分析和 分析和修 析和修复 和修复思 修复思路 不涉 及针 针对 对真 真实 实目 目标 标的 的攻 攻击 击利 利用 用过 过程 不涉及 涉及针 及针对 针对真 对真实 真实目 实目标 目标的 标的攻 的攻击 攻击利 击利用 利用过 用过程 不涉及针 涉及针对 及针对真 针对真实 对真实目 真实目标 实目标的 目标的攻 标的攻击
  19. 背景文件上传是 Web 系统里非常常见的功能,头像、附件、工单截图、导入模板都会涉及。它看起来只是业务能力,但在安全审计里经常是高风险入口:一旦校验逻辑不完整,可能引发任意文件写入、存储型 XSS、服务端解析异常、敏感文件覆盖等问题。 这篇文章整理一次比较典型的文件上传风险排查思路,重点放在代码审计和防护设计上。内容不会涉及未授权入侵或攻击利用细节,更多是从研发和安全评审角度,说明如何判断一个上传功能是否可靠。 常见场景在实际项目里,文件上传通常存在几类实现方式: 后端应用直接接收文件并保存到本地目录。后端接收后转存到对象存储,例如 S3、OSS、COS、MinIO。前端直传对象存储,后端只负责签发上传凭证。上传后进入异步处理流程,例如图片压缩、文档解析、病毒扫描。不同实现方式风险点不完全一样,但核心问题基本围绕三个方面:文件内容是否可信、文件路径是否可控、文件访问是否安全。 问题分析审计上传功能时,不建议只看扩展名判断。扩展名是用户可控输入,只能作为辅助信息。更稳妥的方式是从以下几个维度交叉验证。 一、文件类型校验不能只依赖后缀很多历史代码会这样判断: String filename = file.getOriginalFilename(); if (filename.endsWith(".jpg") || filename.endsWith(".png")) { file.transferTo(new File(uploadDir, filename)); }这个逻辑的问题比较明显:文件名完全来自客户端,攻击者可以构造特殊名称;同时,后缀无法证明文件内容是真正的图片。 更可靠的做法是至少同时检查 MIME、文件头特征和实际解码结果。以图片为例,可以用标准图片库尝试解析,并限制宽高、大小、格式。 BufferedImage image = ImageIO.read(file.getInputStream()); if (image == null) { throw new IllegalArgumentException("invalid image"); } int width = image.getWidth(); int height = image.getHeight(); if (width <= 0 || height <= 0 || width > 8000 || height > 8000) { throw new IllegalArgumentException("invalid image size"); }注意,MIME 头也来自请求,不应单独作为信任依据。它的价值更多是辅助判断和记录日志。 二、文件名和路径必须重新生成上传保存时,最容易被忽略的是路径拼接。以下写法在代码审计中比较常见: File target = new File(uploadDir + "/" + file.getOriginalFilename()); file.transferTo(target);这里存在两个问题:一是原始文件名可能包含路径分隔符;二是可能导致文件覆盖。安全做法是服务端生成随机文件名,并将用户原始文件名仅作为展示字段保存到数据库。 String ext = detectSafeExtension(file); String safeName = UUID.randomUUID().toString().replace("-", "") + ext; Path basePath = Paths.get(uploadDir).toAbsolutePath().normalize(); Path targetPath = basePath.resolve(safeName).normalize(); if (!targetPath.startsWith(basePath)) { throw new IllegalArgumentException("invalid upload path"); } Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);这里的关键点是 normalize 后再检查 startsWith,避免路径穿越导致文件落到预期目录之外。 三、上传目录不要具备脚本执行能力如果上传目录位于 Web 根目录下,并且应用服务器会解析其中某些脚本类型,那么风险会被放大。更推荐的方式是: 上传文件存储在 Web 根目录之外。通过受控下载接口读取文件。对象存储桶默认私有,访问通过短期签名 URL 或后端代理。静态资源域名与主站域名隔离,降低 XSS 影响面。Nginx 可以显式禁止上传目录下的脚本解析。示例仅作思路参考: location /uploads/ { alias /data/app/uploads/; autoindex off; add_header X-Content-Type-Options nosniff; } location ~* ^/uploads/.*\.(php|jsp|jspx|asp|aspx|cgi|pl)$ { return 403; }如果业务允许公开访问图片,也建议设置准确的 Content-Type,并加上 X-Content-Type-Options: nosniff,避免浏览器进行内容嗅探。 四、大小、数量和解压类文件要做限制上传功能不仅有代码执行类风险,也有资源消耗风险。例如超大文件、批量上传、压缩包炸弹、图片解析消耗过高等。实际防护里建议加入: 单文件大小限制。单用户或单 IP 上传频率限制。总存储配额限制。图片宽高和像素总量限制。压缩包解压后的文件数量、总大小、目录层级限制。对于 ZIP 处理,不能简单相信压缩前大小。需要边解压边统计,超过阈值立刻中断。 long totalSize = 0; int fileCount = 0; while ((entry = zipInputStream.getNextEntry()) != null) { if (entry.isDirectory()) { continue; } fileCount++; if (fileCount > 1000) { throw new IllegalArgumentException("too many files"); } long written = copyWithLimit(zipInputStream, outputStream, 10 * 1024 * 1024); totalSize += written; if (totalSize > 100 * 1024 * 1024) { throw new IllegalArgumentException("zip too large"); } }五、文件下载接口也要一起审计很多时候上传保存本身没有明显问题,但下载接口存在越权或任意文件读取。常见风险包括: 通过 fileId 下载时没有校验当前用户权限。通过 path 参数读取文件,导致路径穿越。返回文件时 Content-Disposition 处理不当,引发响应头注入。私有文件使用永久公开 URL,无法撤销访问。较稳妥的做法是下载接口只接收文件 ID,通过数据库查询真实存储路径,并校验业务权限。 Attachment attachment = attachmentService.getById(fileId); if (attachment == null || !permissionService.canRead(currentUser, attachment)) { throw new AccessDeniedException("no permission"); } Path filePath = storage.resolve(attachment.getStorageName()); return download(filePath, attachment.getOriginalName());关键审计步骤实际排查时,可以按下面的顺序进行,效率会比较高。 定位所有上传入口,包括后台、接口、富文本、导入功能和移动端接口。确认文件最终保存位置,是本地目录还是对象存储。检查文件名是否由服务端生成,是否存在路径拼接。检查类型校验是否只依赖后缀或 Content-Type。检查上传目录是否可被 Web 服务器解析执行。检查是否存在大小、频率、配额限制。检查下载接口是否有权限校验和路径约束。查看日志中是否记录上传人、文件 ID、大小、类型、存储路径、处理结果。安全基线建议检查项建议做法文件名服务端随机生成,原始文件名仅用于展示保存路径固定目录,normalize 后校验路径边界类型判断后缀、文件头、解析结果多重校验访问控制私有文件必须鉴权,避免永久公开链接执行权限上传目录禁止脚本执行,尽量放在 Web 根目录外资源限制限制大小、数量、频率、解压后总量日志审计记录上传主体、文件信息、处理结果和异常原因注意点上传安全不是一个 if 判断能解决的问题,而是一组边界控制:入口校验、存储隔离、访问鉴权、解析限制和日志审计都要同时考虑。 不要把前端校验当作安全措施,前端只能改善用户体验。不要信任原始文件名、MIME、后缀、压缩包内路径。不要让上传文件和应用代码处在同一执行上下文。涉及文档预览、图片处理时,要关注第三方解析库漏洞和补丁更新。如果有杀毒或内容检测流程,建议异步处理并设置隔离状态,未检测通过前不要公开访问。总结文件上传风险的根源在于“把不可信内容带入服务端存储和解析流程”。审计时不能只看能不能上传某个后缀,更要看文件从进入系统到被访问、被解析、被下载的完整链路。 比较可靠的方案是:服务端生成文件名,固定存储目录,做内容级校验,上传目录不具备执行能力,下载接口按业务权限控制,并对资源消耗做上限约束。这样即使某个单点校验出现遗漏,也不会轻易演变成高危问题。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 系统 统里 里非 非常 常常 常见 见的 的功 功能 系统里 统里非 里非常 非常常 常常见 常见的 见的功 的功能 系统里非 统里非常 里非常常 非常常见 常常见的 常见的功 见的功能 头像 附件 工单 单截 截图 工单截 单截图 工单截图 导入 入模 模板 板都 都会 会涉 涉及 导入模 入模板 模板都 板都会 都会涉 会涉及 导入模板 入模板都 模板都会 板都会涉 都会涉及 它看 看起 起来 来只 只是 是业 业务 务能 能力 它看起 看起来 起来只 来只是 只是业 是业务 业务能 务能力 它看起来 看起来只 起来只是 来只是业 只是业务 是业务能 业务能力 但在 在安 安全 全审 审计 计里 里经 经常 常是 是高 高风 风险 险入 入口 但在安 在安全 安全审 全审计 审计里 计里经 里经常 经常是 常是高 是高风 高风险 风险入 险入口 但在安全 在安全审 安全审计 全审计里 审计里经 计里经常 里经常是 经常是高 常是高风 是高风险 高风险入 风险入口 一旦 旦校 校验 验逻 逻辑 辑不 不完 完整 一旦校 旦校验 校验逻 验逻辑 逻辑不 辑不完 不完整 一旦校验 旦校验逻 校验逻辑 验逻辑不 逻辑不完 辑不完整 可能 能引 引发 发任 任意 意文 件写 写入 可能引 能引发 引发任 发任意 任意文 意文件 文件写 件写入 可能引发 能引发任 引发任意 发任意文 任意文件 意文件写 文件写入 存储 储型 存储型 服务 务端 端解 解析 析异 异常 服务端 务端解 端解析 解析异 析异常 服务端解 务端解析 端解析异 解析异常 敏感 感文 件覆 覆盖 盖等 等问 问题 敏感文 感文件 文件覆 件覆盖 覆盖等 盖等问 等问题 敏感文件 感文件覆 文件覆盖 件覆盖等 覆盖等问 盖等问题 这篇 篇文 文章 章整 整理 理一 一次 次比 比较 较典 典型 型的 的文 传风 险排 排查 查思 思路 这篇文 篇文章 文章整 章整理 整理一 理一次 一次比 次比较 比较典 较典型 典型的 型的文 的文件 上传风 传风险 风险排 险排查 排查思 查思路 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次比 一次比较 次比较典 比较典型 较典型的 典型的文 型的文件 的文件上 件上传风 上传风险 传风险排 风险排查 险排查思 排查思路 重点 点放 放在 在代 代码 码审 计和 和防 防护 护设 设计 计上 重点放 点放在 放在代 在代码 代码审 码审计 审计和 计和防 和防护 防护设 护设计 设计上 重点放在 点放在代 放在代码 在代码审 代码审计 码审计和 审计和防 计和防护 和防护设 防护设计 护设计上 内容 容不 不会 及未 未授 授权 权入 入侵 侵或 或攻 攻击 击利 利用 用细 细节 内容不 容不会 不会涉 涉及未 及未授 未授权 授权入 权入侵 入侵或 侵或攻 或攻击 攻击利 击利用 利用细 用细节 内容不会 容不会涉 不会涉及 会涉及未 涉及未授 及未授权 未授权入 授权入侵 权入侵或 入侵或攻 侵或攻击 或攻击利 攻击利用 击利用细 利用细节 更多 多是 是从 从研 研发 发和 和安 全评 评审 审角 角度 更多是 多是从 是从研 从研发 研发和 发和安 和安全 安全评 全评审 评审角 审角度 更多是从 多是从研 是从研发 从研发和 研发和安 发和安全 和安全评 安全评审 全评审角 评审角度 说明 明如 如何 何判 判断 断一 一个 个上 传功 能是 是否 否可 可靠 说明如 明如何 如何判 何判断 判断一 断一个 一个上 个上传 上传功 传功能 功能是 能是否 是否可 否可靠 说明如何 明如何判 如何判断 何判断一 判断一个 断一个上 一个上传 个上传功 上传功能 传功能是 功能是否 能是否可 是否可靠 见场 场景 景在 在实 实际 际项 项目 目里 常见场 见场景 场景在 景在实 在实际 实际项 际项目 项目里 常见场景 见场景在 场景在实 景在实际 在实际项 实际项目 际项目里 传通 通常 常存 存在 在几 几类 类实 实现 现方 方式 上传通 传通常 通常存 常存在 存在几 在几类 几类实 类实现 实现方 现方式 件上传通 上传通常 传通常存 通常存在 常存在几 存在几类 在几类实 几类实现 类实现方 实现方式 后端 端应 应用 用直 直接 接接 接收 收文 件并 并保 保存 存到 到本 本地 地目 目录 后端应 端应用 应用直 用直接 直接接 接接收 接收文 收文件 文件并 件并保 并保存 保存到 存到本
  20. 服务端请求伪造(SSRF)并不是简单的“用户可控 URL”问题。真正决定风险的,通常是 URL 解析方式、DNS 解析行为、重定向处理、网络出口策略以及响应内容是否会返回给用户。很多修复只增加了一个内网 IP 判断,实际仍可能被 IPv6、十进制 IP、DNS 重绑定或重定向绕过。本文以代码审计和防守验证为主线,整理一套适用于 Java Web 服务的 SSRF 排查方法。示例均应在本地靶场或经授权的测试环境中进行,不直接访问生产内网、云平台元数据服务或其他未授权目标。一、常见业务场景与风险边界SSRF 通常出现在以下功能中:远程图片、头像、附件抓取;Webhook、回调地址或第三方通知配置;URL 预览、网页截图、Markdown 远程资源解析;导入远程 XML、JSON、RSS 或 OpenAPI 文档;代理下载、在线转码和远程健康检查。审计时不要只搜索 HttpClient 或 RestTemplate。更重要的是追踪数据流:用户输入是否进入 URL 构造函数,是否进入 DNS 解析、TCP 连接或 HTTP 客户端,响应是否被读取并返回给攻击者。判断 SSRF 的关键不是“是否发送了 HTTP 请求”,而是“请求的目标地址和请求内容是否被低可信输入影响,以及服务端网络权限是否超过了业务需要”。二、代码审计的入口与数据流可以先从高风险 API 入手,再回溯参数来源。Java 项目中常见的请求点包括:java.net.URL、java.net.URI;Apache HttpClient、OkHttp、WebClient、RestTemplate;文件下载工具、图片解析器和 HTML 渲染器;自定义的 download、fetch、proxy、callback 方法。建议将审计过程分为三步。第一步确认输入来源,例如 HTTP 参数、JSON 字段、数据库中的管理员配置或消息队列内容。第二步确认是否存在规范化和校验。第三步确认真正建立连接时使用的地址是否仍然来自原始输入。String target = request.getParameter(&quot;url&quot;); URL url = new URL(target); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setReadTimeout(5000); InputStream input = connection.getInputStream();上面的代码存在多个审计疑点:没有限制协议;没有校验主机;没有处理重定向;DNS 解析和实际连接之间可能发生变化;读取响应时也没有限制大小。即使增加一个简单的字符串判断,也不能认为问题已经解决。三、URL 校验中最容易忽略的问题1. 不要使用字符串前缀判断以下做法都不可靠:if (url.startsWith(&quot; { // 允许访问 }字符串比较无法正确区分主机、端口、用户名信息和路径。例如,可信域名后面附加其他主机名、使用用户信息字段或改变大小写,都可能导致判断结果与实际连接目标不一致。应当先使用标准解析器解析,再对结构化字段进行校验。2. 只允许业务需要的协议如果业务只是抓取网页,通常只需要 http 和 https。应显式拒绝 file、jar、ftp、gopher 等不必要协议,同时注意底层 HTTP 客户端或第三方库是否支持自定义协议处理器。URI uri = URI.create(input); String scheme = uri.getScheme(); if (!&quot;http&quot;.equalsIgnoreCase(scheme) &amp;&amp; !&quot;https&quot;.equalsIgnoreCase(scheme)) { throw new IllegalArgumentException(&quot;unsupported scheme&quot;); }3. 校验主机名,而不是只校验解析结果对于固定合作方,最稳妥的方式是使用允许列表,例如允许的完整域名、端口和路径范围。对于必须支持任意公网地址的场景,至少需要解析主机名并拒绝回环地址、链路本地地址、私有地址、保留地址和未指定地址。需要同时考虑 IPv4 和 IPv6。常见错误包括只判断 127.0.0.1,却没有覆盖 IPv6 回环地址;只判断字符串形式的私网网段,却没有处理不同格式的 IP 表示;只校验一次 DNS 结果,却在后续连接阶段重新解析。4. 注意 DNS 重绑定和解析时序典型的脆弱流程是:程序先解析域名并判断为公网地址,随后 HTTP 客户端再次解析同一域名并建立连接。如果两次解析结果不同,校验和连接使用的就不是同一个地址。防守侧可以采用以下策略:在解析阶段获取所有 A 和 AAAA 记录,逐一进行地址分类;连接时使用已经校验过的地址,避免再次根据原始域名解析;通过受控 DNS 解析器和网络出口策略做第二层限制;对重定向后的每个目标重新执行完整校验。在 Java 中,直接调用 InetAddress.getByName 并不能自动解决时序问题。关键在于校验后的地址是否真正被连接层使用,以及客户端是否会在内部重新解析域名。四、重定向是经常被遗漏的第二跳很多 HTTP 客户端默认跟随 301、302、307 或 308 重定向。如果只校验初始 URL,服务端可能先访问一个允许的站点,再被重定向到不应访问的地址。建议关闭自动重定向,由业务代码逐跳处理:HttpClient client = HttpClients.custom() .disableRedirectHandling() .setDefaultRequestConfig( RequestConfig.custom() .setConnectTimeout(3000) .setResponseTimeout(5000) .build()) .build();收到重定向响应后,应读取 Location,解析为绝对地址或基于原地址规范化后的新地址,然后重新执行协议、主机、端口和 IP 分类检查。重定向次数也应设置上限,通常一到三次已经足够。五、推荐的防护分层SSRF 不应只依赖一段业务代码修复,建议建立多层控制。第一层:业务允许列表能使用固定目标列表时,不要开放任意 URL。允许列表应至少包含协议、主机、端口,必要时还应限制路径。域名匹配要使用完整标签边界,不能简单使用后缀匹配。boolean allowedHost(String host) { return &quot;api.partner.example&quot;.equalsIgnoreCase(host) || &quot;storage.partner.example&quot;.equalsIgnoreCase(host); }第二层:地址分类与连接前校验对域名解析得到的每个地址进行分类,拒绝回环、私有、链路本地、组播、保留和未指定地址。还要限制端口范围,避免业务只需要 443 却允许访问任意内部服务端口。第三层:HTTP 客户端限制禁用不需要的协议和重定向;设置连接、读取和连接池超时;限制响应头和响应体大小;限制单次请求的最大下载时间;禁止将内部请求凭据自动转发到外部目标。第四层:网络出口控制在容器、虚拟机或主机防火墙层面限制服务的出站访问,只允许业务所需的 DNS、代理和目标网段。网络策略是代码防护失效后的最后一道边界,也能降低 SSRF 进一步访问内部管理面板、消息队列和数据库的风险。六、一个更完整的校验思路下面的代码是审计和设计时的简化示意,不建议未经评估直接复制到生产环境。生产实现还需要处理 DNS 缓存、IPv6、代理配置、连接复用和客户端内部解析行为。URI uri = URI.create(input); if (uri.getUserInfo() != null) { throw new IllegalArgumentException(&quot;userinfo is not allowed&quot;); } String scheme = uri.getScheme(); if (!&quot;http&quot;.equalsIgnoreCase(scheme) &amp;&amp; !&quot;https&quot;.equalsIgnoreCase(scheme)) { throw new IllegalArgumentException(&quot;scheme is not allowed&quot;); } String host = uri.getHost(); if (host == null || host.length() == 0) { throw new IllegalArgumentException(&quot;invalid host&quot;); } int port = uri.getPort(); if (port != -1 &amp;&amp; port != 80 &amp;&amp; port != 443) { throw new IllegalArgumentException(&quot;port is not allowed&quot;); } for (InetAddress address : DnsResolver.resolveAll(host)) { if (AddressPolicy.isPrivateOrReserved(address)) { throw new IllegalArgumentException(&quot;address is not allowed&quot;); } }实际连接时,还要确保 HTTP 客户端使用的是已校验的解析结果,或者通过受控代理统一发起请求。若无法保证客户端不会重新解析域名,应优先采用出口代理方案,并在代理层再次执行目标策略。七、响应处理同样需要安全边界SSRF 风险不只在“请求发到哪里”,还在于响应如何被处理。建议关注以下问题:限制响应体大小,防止大文件导致内存或磁盘耗尽;不要直接把内部服务响应原样返回给前端;按业务需要限制响应内容类型,图片抓取不应接受任意脚本或可执行格式;下载到临时目录时使用随机文件名,并设置权限和生命周期;对压缩文件、XML 和图片解析设置资源限制,避免引入其他解析类漏洞;记录目标域名、最终地址、状态码、响应大小和耗时,但不要记录敏感响应正文。如果业务只是判断 URL 是否可用,可以只读取响应头或少量内容,不必将整个响应加载到内存。对于异步抓取任务,应增加任务级超时、并发上限和失败重试上限。八、审计与验证清单检查项关注点输入来源参数、JSON、数据库配置、消息队列是否可被低可信用户影响协议限制是否只允许 http/https,是否存在自定义协议处理器主机校验是否使用结构化解析,是否覆盖 IPv4、IPv6 和域名DNS 行为校验和连接是否可能发生二次解析,是否考虑解析结果变化重定向是否关闭自动跟随,跳转后的目标是否重新校验端口控制是否限制到业务需要的端口,是否存在任意端口访问响应处理是否限制大小、类型、超时,并避免直接回显内部内容网络策略服务是否具备不必要的内网和管理网段出站权限九、在测试环境中的验证方法验证时建议搭建两个本地服务:一个作为允许访问的正常站点,另一个作为重定向站点或记录请求的观察端。测试目标是确认校验逻辑是否按预期拒绝不允许的协议、地址、端口和跳转,而不是扫描真实内网。python3 -m http.server 8080 --bind 127.0.0.1可以在单元测试中覆盖以下类别:合法公网域名、明确的本地回环地址、IPv6 回环地址、私有地址、无效端口、带用户名信息的 URL、初始地址合法但重定向地址不合法的场景,以及超大响应和长时间不返回的场景。测试结果应验证请求是否被阻断、日志是否完整、服务是否能够及时释放连接。十、日志、告警与事件复盘对可能发起外部请求的服务,建议记录结构化审计日志,包括请求来源、业务接口、目标主机、解析地址、最终地址、端口、状态码、耗时、响应大小和拒绝原因。日志中的查询参数和响应内容应进行脱敏。告警可以关注短时间内大量不同目标、访问异常端口、频繁触发地址策略拒绝、请求耗时明显升高以及服务向不常见公网网段发起连接等情况。复盘时要同时检查代码、依赖库默认行为、容器网络策略和云环境出站权限,避免只修补某一个 URL 参数。总结SSRF 的有效防护需要把 URL 解析、DNS、连接建立、重定向、响应处理和网络出口看成一条完整链路。单独增加黑名单、判断字符串前缀或只拦截某个内网地址,都很难覆盖真实环境中的边界情况。更可靠的实践是:优先使用业务允许列表;严格限制协议、主机和端口;对所有解析结果执行地址分类;关闭自动重定向并逐跳校验;限制响应资源;最后通过代理和网络策略建立出口边界。代码审计时沿着“输入来源—URL 构造—解析—连接—响应”的数据流检查,通常比单纯搜索危险函数更容易发现完整问题。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不是 是简 简单 单的 并不是 不是简 是简单 简单的 并不是简 不是简单 是简单的 用户 户可 可控 用户可 户可控 用户可控 问题 真正 正决 决定 定风 风险 险的 真正决 正决定 决定风 定风险 风险的 真正决定 正决定风 决定风险 定风险的 通常 常是 通常是 解析 析方 方式 解析方 析方式 解析方式 析行 行为 解析行 析行为 解析行为 重定 定向 向处 处理 重定向 定向处 向处理 重定向处 定向处理 网络 络出 出口 口策 策略 略以 以及 及响 响应 应内 内容 容是 是否 否会 会返 返回 回给 给用 网络出 络出口 出口策 口策略 策略以 略以及 以及响 及响应 响应内 应内容 内容是 容是否 是否会 否会返 会返回 返回给 回给用 给用户 网络出口 络出口策 出口策略 口策略以 策略以及 略以及响 以及响应 及响应内 响应内容 应内容是 内容是否 容是否会 是否会返 否会返回 会返回给 返回给用 回给用户 很多 多修 修复 复只 只增 增加 加了 了一 一个 个内 内网 很多修 多修复 修复只 复只增 只增加 增加了 加了一 了一个 一个内 个内网 很多修复 多修复只 修复只增 复只增加 只增加了 增加了一 加了一个 了一个内 一个内网 判断 实际 际仍 仍可 可能 能被 实际仍 际仍可 仍可能 可能被 实际仍可 际仍可能 仍可能被 十进 进制 十进制 重绑 绑定 定或 或重 向绕 绕过 重绑定 绑定或 定或重 或重定 定向绕 向绕过 重绑定或 绑定或重 定或重定 或重定向 重定向绕 定向绕过 本文 文以 以代 代码 码审 审计 计和 和防 防守 守验 验证 证为 为主 主线 本文以 文以代 以代码 代码审 码审计 审计和 计和防 和防守 防守验 守验证 验证为 证为主 为主线 本文以代 文以代码 以代码审 代码审计 码审计和 审计和防 计和防守 和防守验 防守验证 守验证为 验证为主 证为主线 整理 理一 一套 套适 适用 用于 整理一 理一套 一套适 套适用 适用于 整理一套 理一套适 一套适用 套适用于 务的 服务的 排查 查方 方法 排查方 查方法 排查方法 示例 例均 均应 应在 在本 本地 地靶 靶场 场或 或经 经授 授权 权的 的测 测试 试环 环境 境中 中进 进行 示例均 例均应 均应在 应在本 在本地 本地靶 地靶场 靶场或 场或经 或经授 经授权 授权的 权的测 的测试 测试环 试环境 环境中 境中进 中进行 示例均应 例均应在 均应在本 应在本地 在本地靶 本地靶场 地靶场或 靶场或经 场或经授 或经授权 经授权的 授权的测 权的测试 的测试环 测试环境 试环境中 环境中进 境中进行 不直 直接 接访 访问 问生 生产 产内 不直接 直接访 接访问 访问生 问生产 生产内 产内网 不直接访 直接访问 接访问生 访问生产 问生产内 生产内网 云平 平台 台元 元数 数据 据服 务或 或其 其他 他未 未授 权目 目标 云平台 平台元 台元数 元数据 数据服 据服务 服务或 务或其 或其他 其他未 他未授 未授权 授权目 权目标 云平台元 平台元数 台元数据 元数据服 数据服务 据服务或 服务或其 务或其他 或其他未 其他未授 他未授权 未授权目 授权目标 常见 见业 业务 务场 场景 景与 与风 险边 边界 常见业 见业务 业务场 务场景 场景与 景与风 与风险 风险边 险边界 常见业务 见业务场 业务场景 务场景与 场景与风 景与风险 与风险边 风险边界 常出 出现 现在 在以 以下 下功 功能 能中 通常出 常出现 出现在 现在以 在以下 以下功 下功能 功能中 通常出现 常出现在 出现在以 现在以下 在以下功 以下功能 下功能中 远程 程图 图片 远程图 程图片 远程图片 头像 附件 件抓 抓取 附件抓 件抓取 附件抓取 回调 调地 地址 址或 或第 第三 三方 方通 通知 知配 配置 回调地 调地址 地址或 址或第 或第三 第三方 三方通 方通知 通知配 知配置 回调地址 调地址或 地址或第 址或第三 或第三方 第三方通 三方通知 方通知配 通知配置 预览 网页 页截 截图 网页截 页截图 网页截图 程资 资源 源解 远程资 程资源 资源解 源解析 远程资源 程资源解 资源解析 导入 入远 导入远 入远程 导入远程 文档 代理 理下 下载 代理下 理下载 代理下载 在线 线转 转码 码和 和远 程健 健康 康检 检查 在线转 线转码
  21. 服务端请求伪造(SSRF)并不只是一个“能否访问内网”的简单问题。实际审计中,风险通常分散在 URL 解析、DNS 解析、重定向、代理配置和网络出口策略等多个环节。单纯增加一个正则表达式,往往只能拦住最直观的测试用例,却无法覆盖 IPv6、DNS 重绑定、编码差异以及跳转链等场景。 本文以一个常见的“远程图片抓取”功能为例,整理从代码定位、风险确认、修复设计到上线验证的完整思路。示例仅用于本地测试环境和防御性审计,不涉及访问真实内网服务或敏感信息。 一、问题背景与典型场景很多业务会提供类似功能:用户提交一个图片或文档 URL,后端下载资源后进行转码、缓存或内容审核。伪代码通常如下: def fetch_resource(url): response = http_client.get(url, timeout=10) return response.content如果这个 URL 完全由用户控制,服务端就可能成为一个网络请求代理。风险主要包括: 访问本不应由公网用户触达的管理接口、监控接口或内部服务。将内网服务的响应内容带回前端,造成信息泄露。借助重定向、代理或特殊地址表示绕过简单的域名黑名单。消耗服务端连接、带宽和线程资源,形成应用层资源滥用。需要注意的是,SSRF 是否构成高危,取决于网络分区、服务权限、响应处理方式和出口策略。一个只能访问固定公网域名的抓取服务,与一个可以访问整个内部网络的服务,实际影响完全不同。 二、代码审计时的定位方法审计时不要只搜索“requests.get”或“HttpClient”。更有效的方法是从用户可控输入开始,沿数据流追踪到网络请求函数。 查找输入来源:HTTP 参数、JSON 字段、文件内容、Webhook 地址、Markdown 链接以及任务配置。确认是否经过拼接或二次编码,例如 URL 被放入代理参数、重定向参数或 HTML 解析流程。追踪最终请求函数,记录是否允许自动跳转、是否使用系统代理、是否验证 TLS、是否设置超时。检查响应是否会被原样返回、写入日志、保存到对象存储,或交给后续解析器处理。确认运行环境的网络权限,包括容器网段、云平台元数据访问、服务账号权限和出站防火墙策略。在审计记录中,建议同时保留“输入点—校验点—解析点—请求点—输出点”五个位置。很多漏洞并非没有校验,而是校验发生在错误的解析阶段,或者校验后的 URL 在后续流程中被重新解析。 三、URL 校验中最容易出错的地方1. 只做字符串匹配类似 url.startswith(" 的判断不等价于域名校验。字符串前缀可能匹配到相似域名、用户名字段或路径内容。正确做法是使用标准 URL 解析器,分别检查 scheme、hostname、port 和 path。 2. 只检查域名,不检查解析结果域名最终会解析为 IP 地址。即使域名本身看起来可信,也需要确认解析结果是否落入私网、回环、链路本地、保留地址或其他不允许的网络范围。IPv4 映射 IPv6 地址、十进制或十六进制地址表示,也不能依赖字符串规则判断。 3. 忽略重定向初始地址可能是允许访问的公网地址,但服务端跟随 30x 跳转后,目标已经发生变化。因此需要关闭自动重定向,或者对每一次跳转重新执行完整校验,并限制最大跳转次数。 4. 校验与连接之间存在时间差先 DNS 解析、再发起请求的流程存在 DNS 重绑定风险。一次解析得到公网 IP,并不意味着连接建立时仍然使用同一个地址。对高风险场景,应用层校验应配合固定出口代理、网络 ACL 和连接级地址控制,不能只依赖业务代码。 四、一个防御性校验示例下面的代码展示了基础校验思路,适合作为审计或单元测试的起点。它不是完整的网络隔离方案,生产环境仍应通过受控代理和出站防火墙建立最后一道边界。 from urllib.parse import urlsplit import ipaddress import socket ALLOWED_SCHEMES = {"http", "https"} ALLOWED_PORTS = {80, 443} def host_resolves_to_public_ip(host): try: records = socket.getaddrinfo( host, None, type=socket.SOCK_STREAM ) except socket.gaierror: return False if not records: return False for record in records: address = record[4][0] ip = ipaddress.ip_address(address) if not ip.is_global: return False return True def validate_target(raw_url): parsed = urlsplit(raw_url) if parsed.scheme.lower() not in ALLOWED_SCHEMES: return False if not parsed.hostname: return False if parsed.username or parsed.password: return False try: port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80) except ValueError: return False if port not in ALLOWED_PORTS: return False if not host_resolves_to_public_ip(parsed.hostname): return False return True这个示例包含几个关键点:使用结构化解析而不是字符串拼接;拒绝用户名和密码字段;限制协议与端口;将域名解析为 IP 后检查地址属性;对解析失败和异常端口直接拒绝。 但需要特别强调,getaddrinfo 检查与后续 HTTP 连接之间仍然可能存在时间窗口。更稳妥的架构是让应用只向一个受控抓取代理发请求,由代理统一完成 DNS 解析、地址过滤、连接建立和重定向检查,业务服务本身不直接访问任意目标。 五、请求层的安全控制URL 校验之外,请求客户端还应设置明确的资源边界: 禁止或严格限制自动重定向,限制最大跳转次数。设置连接超时、读取超时、总耗时和最大响应体大小。限制响应内容类型,只允许业务真正需要的类型,例如图片或纯文本。禁止将用户输入直接作为代理地址、请求头或认证信息。对压缩响应、分块传输和异常 Content-Length 做大小保护,避免资源耗尽。下载完成后重新检测文件类型,不要仅依据扩展名或响应头。以 Python 客户端为例,基础配置可以类似这样: response = session.get( target_url, allow_redirects=False, timeout=(3, 8), stream=True, headers={"Accept": "image/*"} ) content_length = response.headers.get("Content-Length") if content_length and int(content_length) > 5 * 1024 * 1024: raise ValueError("response too large")实际读取响应体时,还需要在流式读取过程中累计字节数,不能只依赖 Content-Length,因为服务端可能不返回该字段,或者返回不可信的值。 六、网络层的纵深防御代码修复解决的是应用层逻辑,网络层仍然需要独立控制: 抓取服务使用独立网络区域,默认拒绝访问数据库、缓存、消息队列和管理网段。通过出站防火墙或 egress proxy 只允许访问必要的公网端口和域名。明确禁止访问回环、链路本地、私网、保留地址以及云环境敏感管理地址。为抓取服务使用最小权限的服务账号,避免网络访问能力与云 API 权限叠加。记录目标域名、解析结果、最终连接地址、响应状态、响应大小和耗时,便于追溯。日志中不要保存完整的认证 URL、Cookie 或可能包含敏感参数的请求头。对于用户提交的 URL,可以记录脱敏后的规范化结果和哈希值,既方便关联分析,也能减少二次泄露风险。 七、如何在测试环境中验证修复验证应优先使用自建的多个测试服务,不要把测试请求指向生产内网或第三方基础设施。可以准备以下场景: 正常公网 HTTPS 地址,确认业务功能不受影响。回环地址、私网地址、链路本地地址和 IPv4 映射 IPv6 地址,确认被拒绝。包含用户名字段、非标准端口和异常编码的 URL,确认解析行为一致。返回 30x 的测试服务,确认跳转目标会重新校验。返回超大响应、慢速响应和错误 Content-Type 的测试服务,确认超时与大小限制生效。DNS 解析失败、返回多个地址或地址变化时,确认默认采取拒绝策略。建议把这些场景固化为单元测试和集成测试,尤其关注不同 URL 解析库之间的行为差异。升级运行时、HTTP 客户端或代理组件后,应重新执行这组测试。 八、常见修复误区只维护黑名单:黑名单无法覆盖所有地址表示和未来新增网段,优先采用明确的协议、端口和目标范围白名单。只校验 hostname:必须结合 DNS 结果、重定向目标和实际连接地址判断。认为 HTTPS 可以解决 SSRF:HTTPS 只保护传输和证书校验,并不会阻止请求到达错误的网络位置。把解析逻辑散落在多个业务模块:应集中封装抓取能力,统一超时、大小、跳转和审计策略。只看 HTTP 状态码:响应体大小、内容类型、耗时和解析器行为同样可能造成安全问题。九、总结SSRF 审计的重点不是寻找一个“万能正则”,而是完整理解从用户输入到网络连接的整个链路。可靠的防护通常由四部分组成:严格的 URL 结构校验、对解析结果和跳转目标的持续检查、受控的请求客户端配置,以及网络出口层的默认拒绝策略。 如果业务确实只需要抓取少量固定站点,最安全的方案通常不是允许任意 URL 后不断补丁,而是改成站点白名单或资源标识映射。对于必须支持动态目标的场景,也应优先采用隔离的抓取服务和受控代理,并通过日志与测试用例持续验证边界。这样才能把一次代码缺陷,降低为可监控、可限制、可回滚的工程风险。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不只 只是 是一 一个 并不只 不只是 只是一 是一个 并不只是 不只是一 只是一个 能否 否访 访问 问内 内网 能否访 否访问 访问内 问内网 能否访问 否访问内 访问内网 的简 简单 单问 问题 的简单 简单问 单问题 的简单问 简单问题 实际 际审 审计 计中 实际审 际审计 审计中 实际审计 际审计中 风险 险通 通常 常分 分散 散在 风险通 险通常 通常分 常分散 分散在 风险通常 险通常分 通常分散 常分散在 解析 重定 定向 重定向 代理 理配 配置 置和 和网 网络 络出 出口 口策 策略 略等 等多 多个 个环 环节 代理配 理配置 配置和 置和网 和网络 网络出 络出口 出口策 口策略 策略等 略等多 等多个 多个环 个环节 代理配置 理配置和 配置和网 置和网络 和网络出 网络出口 络出口策 出口策略 口策略等 策略等多 略等多个 等多个环 多个环节 单纯 纯增 增加 加一 个正 正则 则表 表达 达式 单纯增 纯增加 增加一 加一个 一个正 个正则 正则表 则表达 表达式 单纯增加 纯增加一 增加一个 加一个正 一个正则 个正则表 正则表达 则表达式 往往 往只 只能 能拦 拦住 住最 最直 直观 观的 的测 测试 试用 用例 往往只 往只能 只能拦 能拦住 拦住最 住最直 最直观 直观的 观的测 的测试 测试用 试用例 往往只能 往只能拦 只能拦住 能拦住最 拦住最直 住最直观 最直观的 直观的测 观的测试 的测试用 测试用例 却无 无法 法覆 覆盖 却无法 无法覆 法覆盖 却无法覆 无法覆盖 重绑 绑定 重绑定 编码 码差 差异 异以 以及 及跳 跳转 转链 链等 等场 场景 编码差 码差异 差异以 异以及 以及跳 及跳转 跳转链 转链等 链等场 等场景 编码差异 码差异以 差异以及 异以及跳 以及跳转 及跳转链 跳转链等 转链等场 链等场景 本文 文以 以一 个常 常见 见的 本文以 文以一 以一个 一个常 个常见 常见的 本文以一 文以一个 以一个常 一个常见 个常见的 远程 程图 图片 片抓 抓取 远程图 程图片 图片抓 片抓取 远程图片 程图片抓 图片抓取 功能 能为 为例 功能为 能为例 功能为例 整理 理从 从代 代码 码定 定位 整理从 理从代 从代码 代码定 码定位 整理从代 理从代码 从代码定 代码定位 险确 确认 风险确 险确认 风险确认 修复 复设 设计 计到 到上 上线 线验 验证 证的 的完 完整 整思 思路 修复设 复设计 设计到 计到上 到上线 上线验 线验证 验证的 证的完 的完整 完整思 整思路 修复设计 复设计到 设计到上 计到上线 到上线验 上线验证 线验证的 验证的完 证的完整 的完整思 完整思路 示例 例仅 仅用 用于 于本 本地 地测 试环 环境 境和 和防 防御 御性 性审 示例仅 例仅用 仅用于 用于本 于本地 本地测 地测试 测试环 试环境 环境和 境和防 和防御 防御性 御性审 性审计 示例仅用 例仅用于 仅用于本 用于本地 于本地测 本地测试 地测试环 测试环境 试环境和 环境和防 境和防御 和防御性 防御性审 御性审计 不涉 涉及 及访 问真 真实 实内 网服 务或 或敏 敏感 感信 信息 不涉及 涉及访 及访问 访问真 问真实 真实内 实内网 内网服 网服务 服务或 务或敏 或敏感 敏感信 感信息 不涉及访 涉及访问 及访问真 访问真实 问真实内 真实内网 实内网服 内网服务 网服务或 服务或敏 务或敏感 或敏感信 敏感信息 题背 背景 景与 与典 典型 型场 景很 很多 多业 业务 务会 会提 提供 供类 类似 似功 问题背 题背景 背景与 景与典 与典型 典型场 型场景 场景很 景很多 很多业 多业务 业务会 务会提 会提供 提供类 供类似 类似功 似功能 问题背景 题背景与 背景与典 景与典型 与典型场 典型场景 型场景很 场景很多 景很多业 很多业务 多业务会 业务会提 务会提供 会提供类 提供类似 供类似功 类似功能 用户 户提 提交 交一 个图 片或 或文 文档 用户提 户提交 提交一 交一个 一个图 个图片 图片或 片或文 或文档 用户提交 户提交一 提交一个 交一个图 一个图片 个图片或 图片或文 片或文档 后端 端下 下载 载资 资源 源后 后进 进行 行转 转码 后端下 端下载 下载资 载资源 资源后 源后进 后进行 进行转 行转码 后端下载 端下载资 下载资源 载资源后 资源后进 源后进行 后进行转 进行转码 缓存 存或
  22. 服务端请求伪造(SSRF)是代码审计中经常遇到的一类问题。它通常不是由某个危险函数单独造成,而是由“用户可控地址”“服务端发起请求”“缺少目标限制”三个条件共同形成。实际项目里,SSRF 可能出现在图片抓取、Webhook、远程文件导入、URL 预览、文档转换和代理接口等功能中。 这类问题的难点在于:简单的黑名单很容易被绕过,单纯判断字符串是否包含内网地址也不可靠,而修复时还要兼顾 DNS 解析、重定向、IPv6、代理配置和网络分区。本文以代码审计和防守验证为主线,整理一套适合实际项目的分析方法。 一、先明确风险边界审计 SSRF 时,不要一开始就把所有“服务器发起的 HTTP 请求”都判定为漏洞。更准确的判断方式是分析请求目标是否受到攻击者控制,以及服务端能访问到什么网络区域。 高风险场景:接口允许用户提交任意 URL,服务端再将响应内容返回给用户,或将内容解析、保存、转发到其他系统。中风险场景:目标地址可控,但服务端有固定域名白名单;需要进一步确认白名单校验是否可靠。低风险场景:请求目标完全由服务端配置决定,用户只能选择有限的业务对象,且网络层也有访问控制。需要重点确认的影响:是否能够访问管理端口、内部 API、云平台实例元数据服务、容器管理接口或仅限本机访问的调试服务。风险评级不能只看“是否能请求 127.0.0.1”。同一个缺陷在开发环境可能只访问到测试接口,在生产环境则可能触达敏感管理平面,因此必须结合部署拓扑和出网策略判断。 二、代码审计中的入口识别审计时可以从数据流入手,而不是只搜索某个固定函数名。常见入口包括以下几类: HTTP 参数、JSON 字段、表单字段中的 url、uri、callback、webhook、source。上传后触发远程下载、图片转码、网页截图、PDF 转换的业务流程。后台任务、消息队列消费者和定时任务中的远程资源同步逻辑。支持自定义代理、远程仓库、镜像地址的管理功能。将用户输入传入 HTTP 客户端、Socket、RPC、DNS 或命令执行组件的代码。搜索调用点时,除了常见的 HTTP 客户端,还要关注封装层。例如 Java 项目中的 RestTemplate、WebClient、OkHttp,Python 项目中的 requests、urllib、httpx,Node.js 项目中的 axios、fetch,以及项目自定义的 HttpUtil、Downloader、UrlFetcher 等类。 三、建立完整的数据流一个可复现、可修复的审计结论,至少要说明以下数据流: 用户输入从哪个参数进入系统。输入是否经过 URL 解码、拼接、规范化或重定向。最终由哪个组件发起请求。请求使用了什么协议、端口、代理和超时配置。响应内容是否返回给用户,或被写入文件、数据库、缓存。服务端部署网络中有哪些可达目标。例如,下面这段代码的问题不在于使用了某个特定库,而在于完整 URL 由用户输入直接决定: String target = request.getParameter("url"); HttpURLConnection connection = (HttpURLConnection) new URL(target).openConnection(); connection.setConnectTimeout(3000); connection.setReadTimeout(5000); return readBody(connection.getInputStream());即使加入超时,也只能降低资源消耗,不能改变请求目标的可控性。若业务只需要抓取固定站点的公开资源,应该让用户提交业务标识或相对路径,由服务端根据配置生成最终地址,而不是直接接收完整 URL。 四、为什么常见黑名单不可靠实际审计中经常看到以下防护方式: if (url.contains("127.0.0.1") || url.contains("localhost")) { throw new IllegalArgumentException("blocked"); }这种判断存在明显缺陷。地址可能使用不同的表示形式,域名还可能解析到受限制的地址,且初次解析和实际连接之间存在时间窗口。除此之外,IPv6、重定向、代理和 URL 解析差异也会造成过滤结果与实际连接目标不一致。 因此,不建议维护“危险地址字符串列表”。更稳妥的做法是采用正向允许策略,并在解析后的 IP 层面进行判断。防护逻辑至少应覆盖: 只允许业务真正需要的协议,通常优先限制为 https,必要时再开放 http。明确限制端口,避免允许任意端口访问。解析所有 A、AAAA 记录,对每个结果进行内网、回环、链路本地、保留地址和组播地址检查。连接前重新确认解析结果,避免 DNS 解析与连接之间的地址变化。禁止或严格校验重定向,不能只校验第一次请求的目标。关闭不必要的代理继承,避免环境变量或全局代理改变实际请求路径。设置连接、读取、响应大小和总耗时限制。五、推荐的安全请求流程如果业务确实需要访问用户提供的 URL,可以将请求过程拆成几个明确阶段。下面是伪代码,重点是流程而不是某个语言库的具体实现: URI uri = parseStrictly(input); if (!isAllowedScheme(uri.getScheme())) { reject(); } if (uri.getUserInfo() != null) { reject(); } if (uri.getPort() != -1 && !isAllowedPort(uri.getPort())) { reject(); } List<IPAddress> addresses = dnsResolveAll(uri.getHost()); for (IPAddress address : addresses) { if (isPrivateOrReserved(address)) { reject(); } } HttpClient client = buildClientWithoutProxy(); HttpResponse response = client.request(uri, new RequestLimits( connectTimeoutMs = 3000, readTimeoutMs = 5000, maxBodyBytes = 2 * 1024 * 1024, maxRedirects = 0 )); if (response.isRedirect()) { rejectOrRevalidateLocation(response.location()); }其中 isPrivateOrReserved 不应只判断 RFC1918 私有地址,还应覆盖回环地址、链路本地地址、未指定地址、组播地址、文档保留地址以及项目环境中明确不应访问的网段。对于 IPv6,要使用标准地址解析库判断,而不要通过字符串前缀匹配。 六、DNS 与重定向是修复中的重点1. DNS 解析问题域名白名单并不等于安全。某个域名在校验时解析到公开地址,实际连接时可能解析到不同地址;或者域名本身由用户控制,能够通过 DNS 配置指向内部网络。修复时应将“允许的主机名”和“解析后的 IP 校验”结合起来,并尽量在受控网络环境中使用固定解析或出站代理。 对于高安全等级场景,可以让应用不直接访问任意外部地址,而是统一调用一个出站代理。代理负责 DNS 解析、地址分类、审计记录和出站策略,应用只提交经过校验的目标。 2. HTTP 重定向问题即使初始 URL 通过校验,服务器也可能返回 Location,将客户端引导到未授权地址。如果客户端自动跟随重定向,第一次校验就失去意义。 常见的处理方式有两种: 业务不需要重定向时,直接关闭自动跟随,并将重定向视为失败。业务必须支持重定向时,对每一次新的目标重新执行协议、主机、端口和 IP 校验,同时限制重定向次数。七、在本地环境中进行安全验证漏洞验证应优先使用自建测试服务,不要对第三方系统、生产内网或云平台管理接口进行探测。可以准备一个简单的本地测试服务,记录收到的请求,并分别验证以下行为: 正常公开域名是否可以按业务预期访问。回环地址、私有地址和 IPv6 本地地址是否被拒绝。域名解析到受限地址时是否被拒绝。返回重定向到受限地址时,客户端是否停止或重新校验。超大响应、慢连接和异常状态码是否受到限制。URL 中包含用户名密码、非标准端口或不支持协议时是否被拒绝。在测试客户端层面,可以通过单元测试固定解析结果,避免依赖真实 DNS。示例测试命令仅用于验证本地服务的 HTTP 行为: curl --max-time 5 --connect-timeout 3 \ -H 'Accept: application/json' \ ' DNS 请求建立告警。将图片抓取、文档转换等高风险任务放到隔离网络和低权限运行环境。网络层策略的价值在于降低漏洞影响范围,但它不能替代代码中的输入校验。因为应用可能通过代理、解析器、其他协议或内部服务间接访问受限资源。 九、代码审计报告应如何落地一份有价值的报告不应只写“存在 SSRF”,建议至少包含以下内容: 项目说明入口接口、参数、权限要求及触发条件数据流从输入到 URL 解析、DNS、连接和响应处理的过程影响基于实际网络拓扑说明可访问的资源范围根因缺少正向校验、重定向未校验、代理配置不当等修复建议代码、网络、权限和日志层面的具体措施验证结果修复前后测试用例和预期行为修复验证不能只检查“某个字符串被拦截”。更合理的是覆盖一组行为测试,包括不同地址表示、多个 DNS 结果、重定向、异常端口、超时和超大响应。这样可以避免开发人员修复一个样例,却遗漏同一数据流中的其他路径。 十、常见误区误区一:只禁止 localhost。 应检查解析后的地址类别,而不是只匹配一个主机名。误区二:只校验 URL 字符串。 字符串看起来安全,不代表最终连接地址安全。误区三:默认跟随重定向。 每次跳转都可能改变实际访问目标。误区四:只依赖云防火墙。 网络策略可能因环境变化而失效,应用仍需做最小权限控制。误区五:忽略非 HTTP 组件。 图片处理器、PDF 转换器、DNS 客户端和代理组件也可能形成类似风险。误区六:没有限制响应大小和耗时。 即使目标地址安全,也可能造成资源耗尽或任务堆积。总结SSRF 的审计重点不是寻找一个“能否访问内网”的简单结论,而是还原完整的数据流,明确请求目标、解析过程、重定向行为和部署网络边界。修复时应采用正向允许策略,在解析后的 IP 层面校验地址,严格控制协议和端口,并对重定向、DNS 变化、代理继承、超时和响应大小进行统一处理。 从长期治理角度看,最可靠的方案通常是“代码校验 + 出站网络控制 + 最小权限 + 可审计日志 + 自动化回归测试”。这套方法不仅适用于 SSRF,也适用于文件下载、Webhook、远程导入和服务间代理等多个业务场景。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 是代 代码 码审 审计 计中 中经 经常 常遇 遇到 到的 的一 一类 类问 问题 是代码 代码审 码审计 审计中 计中经 中经常 经常遇 常遇到 遇到的 到的一 的一类 一类问 类问题 是代码审 代码审计 码审计中 审计中经 计中经常 中经常遇 经常遇到 常遇到的 遇到的一 到的一类 的一类问 一类问题 它通 通常 常不 不是 是由 由某 某个 个危 危险 险函 函数 数单 单独 独造 造成 它通常 通常不 常不是 不是由 是由某 由某个 某个危 个危险 危险函 险函数 函数单 数单独 单独造 独造成 它通常不 通常不是 常不是由 不是由某 是由某个 由某个危 某个危险 个危险函 危险函数 险函数单 函数单独 数单独造 单独造成 而是 而是由 用户 户可 可控 控地 地址 用户可 户可控 可控地 控地址 用户可控 户可控地 可控地址 端发 发起 起请 务端发 端发起 发起请 起请求 服务端发 务端发起 端发起请 发起请求 缺少 少目 目标 标限 限制 缺少目 少目标 目标限 标限制 缺少目标 少目标限 目标限制 三个 个条 条件 件共 共同 同形 形成 三个条 个条件 条件共 件共同 共同形 同形成 三个条件 个条件共 条件共同 件共同形 共同形成 实际 际项 项目 目里 实际项 际项目 项目里 实际项目 际项目里 可能 能出 出现 现在 在图 图片 片抓 抓取 可能出 能出现 出现在 现在图 在图片 图片抓 片抓取 可能出现 能出现在 出现在图 现在图片 在图片抓 图片抓取 远程 程文 文件 件导 导入 远程文 程文件 文件导 件导入 远程文件 程文件导 文件导入 预览 文档 档转 转换 换和 和代 代理 理接 接口 口等 等功 功能 能中 文档转 档转换 转换和 换和代 和代理 代理接 理接口 接口等 口等功 等功能 功能中 文档转换 档转换和 转换和代 换和代理 和代理接 代理接口 理接口等 接口等功 口等功能 等功能中 这类 题的 的难 难点 点在 在于 这类问 问题的 题的难 的难点 难点在 点在于 这类问题 类问题的 问题的难 题的难点 的难点在 难点在于 简单 单的 的黑 黑名 名单 单很 很容 容易 易被 被绕 绕过 简单的 单的黑 的黑名 黑名单 名单很 单很容 很容易 容易被 易被绕 被绕过 简单的黑 单的黑名 的黑名单 黑名单很 名单很容 单很容易 很容易被 容易被绕 易被绕过 单纯 纯判 判断 断字 字符 符串 串是 是否 否包 包含 含内 内网 网地 址也 也不 不可 可靠 单纯判 纯判断 判断字 断字符 字符串 符串是 串是否 是否包 否包含 包含内 含内网 内网地 网地址 地址也 址也不 也不可 不可靠 单纯判断 纯判断字 判断字符 断字符串 字符串是 符串是否 串是否包 是否包含 否包含内 包含内网 含内网地 内网地址 网地址也 地址也不 址也不可 也不可靠 而修 修复 复时 时还 还要 要兼 兼顾 而修复 修复时 复时还 时还要 还要兼 要兼顾 而修复时 修复时还 复时还要 时还要兼 还要兼顾 解析 重定 定向 重定向 理配 配置 置和 和网 网络 络分 分区 代理配 理配置 配置和 置和网 和网络 网络分 络分区 代理配置 理配置和 配置和网 置和网络 和网络分 网络分区 本文 文以 以代 计和 和防 防守 守验 验证 证为 为主 主线 本文以 文以代 以代码 审计和 计和防 和防守 防守验 守验证 验证为 证为主 为主线 本文以代 文以代码 以代码审 码审计和 审计和防 计和防守 和防守验 防守验证 守验证为 验证为主 证为主线 整理 理一 一套 套适 适合 合实 目的 的分 分析 析方 方法 整理一 理一套 一套适 套适合 适合实 合实际 项目的 目的分 的分析 分析方 析方法 整理一套 理一套适 一套适合 套适合实 适合实际 合实际项 际项目的 项目的分 目的分析 的分析方 分析方法 先明 明确 确风 风险 险边 边界 界审 先明确 明确风 确风险 风险边 险边界 边界审 界审计 先明确风 明确风险 确风险边 风险边界 险边界审 边界审计 不要 要一 一开 开始 始就 就把 把所 所有 不要一 要一开 一开始 开始就 始就把 就把所 把所有 不要一开 要一开始 一开始就 开始就把 始就把所 就把所有 务器 器发 起的 服务器 务器发 器发起 发起的 服务器发 务器发起 器发起的 都判 判定 定为 为漏 漏洞 都判定 判定为 定为漏 为漏洞 都判定为 判定为漏 定为漏洞 更准 准确 确的 的判
  23. SSRF(Server-Side Request Forgery,服务端请求伪造)在代码审计中并不少见。它通常不是一个单独的“危险函数”问题,而是外部输入经过 URL 解析、网络请求、重定向和响应处理等多个环节后,形成了服务端可控的请求能力。真正有效的分析,不能只盯着某个 HTTP 客户端调用,而应当沿着数据流判断:谁能控制请求地址、请求会发往哪里、是否允许重定向、响应是否会回显,以及修复是否覆盖了所有变体。一、问题场景与风险边界常见业务包括图片抓取、Webhook 回调、远程文件导入、站点连通性检测和第三方接口代理。例如,应用提供如下接口:POST /api/import { \"source\": \" /> }服务端为了读取远程内容,可能直接使用用户提交的地址发起请求。如果缺少目标限制,攻击者就可能诱导服务端访问本不应由外部访问的网络资源,例如内部管理接口、云环境元数据地址或仅绑定在回环地址上的调试服务。这里需要区分两个问题:服务端是否能向攻击者无法直接访问的网络位置发起请求;请求结果是否会返回给攻击者,或者是否会改变服务端状态。即使响应内容不直接回显,盲 SSRF 仍可能通过状态码、响应时间、错误信息或业务副作用暴露信息,因此不能简单地以“接口没有返回正文”作为安全结论。二、代码审计时如何定位数据流审计可以从出网点反向追踪。常见出网点包括 Java 的 HttpClient、RestTemplate、WebClient,Python 的 requests、urllib,以及 Node.js 的 axios、fetch。定位到调用点后,重点检查 URL 是否来自请求参数、数据库字段、消息队列或可被普通用户间接控制的配置。以 Java 代码为例,下面的实现存在典型问题:public byte[] download(String source) throws IOException { URL url = new URL(source); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setReadTimeout(5000); return connection.getInputStream().readAllBytes(); }这段代码的问题不在于使用了某个特定类,而在于缺少完整的目标校验:没有限制协议,理论上可能接受非 HTTP 协议或产生解析差异;没有校验解析后的 IP 是否属于受限地址段;没有处理 DNS 解析结果变化;没有限制重定向,初始地址合法并不代表最终地址合法;没有限制响应大小,可能被利用造成资源消耗;异常信息和响应内容可能将内部信息带回前端。三、容易遗漏的解析与校验细节1. 不要只做字符串前缀判断类似 source.startsWith(\" 的判断不可靠。它无法正确处理用户名字段、端口、大小写、尾随点、编码字符以及相似域名等情况。安全校验应基于标准 URL 解析结果,分别检查协议、主机、端口和解析地址。2. 域名校验不能替代 IP 校验即使业务只允许访问某个域名,也要考虑域名解析到内网地址的情况。更稳妥的做法是先解析主机名,再对所有解析结果进行地址分类,拒绝回环地址、链路本地地址、私有地址、未指定地址以及企业明确禁止的网段。下面是一个用于审计思路说明的简化示例,实际项目还应补充 IPv4、IPv6、DNS 变化和异常处理:URI target = URI.create(source); if (!\"https\".equalsIgnoreCase(target.getScheme())) { throw new IllegalArgumentException(\"unsupported scheme\"); } String host = target.getHost(); if (host == null || host.length() &gt; 253) { throw new IllegalArgumentException(\"invalid host\"); } for (InetAddress address : InetAddress.getAllByName(host)) { if (address.isAnyLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) { throw new IllegalArgumentException(\"restricted address\"); } }这个示例不能直接当作通用安全组件使用。生产实现应采用经过审查的网络地址判断库,并结合企业网络边界策略。尤其在 IPv6 环境中,不能只覆盖常见的 IPv4 私有网段。3. 重定向是独立的校验点请求初始地址通过校验后,服务端仍可能被 30x 响应引导到另一个地址。因此应关闭自动重定向,或者对每一次跳转重新执行完整校验,并设置最大跳转次数。HttpClient client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(3)) .build();如果业务必须支持重定向,建议显式读取 Location,限制跳转次数,并对新地址重新执行协议、域名、解析地址和端口校验,而不是简单开启自动跟随。四、修复方案应当分层设计单一校验通常很容易被遗漏,比较稳妥的方式是将控制措施分成业务层、解析层、网络层和资源层。层次建议措施目的业务层优先使用资源 ID 或固定白名单,不直接接收任意 URL减少用户可控范围解析层限制 HTTPS,校验主机、端口、路径和解析地址避免解析绕过请求层关闭或严格控制重定向,设置连接和读取超时降低跳转与阻塞风险网络层使用独立出口代理或 egress ACL,默认拒绝访问内部网段即使代码失误也保留边界资源层限制响应大小、并发数、下载时间和响应类型防止资源消耗其中网络层控制非常重要。代码校验难以覆盖所有解析差异,而出站防火墙、容器网络策略或专用代理可以提供第二道防线。应用不应直接拥有访问管理网段、数据库网段和云平台控制面的能力。五、如何设计安全验证用例验证修复时,不建议只测试一个正常地址。应在隔离测试环境中构造一组输入,观察应用是否拒绝、是否产生出站请求,以及日志是否记录了足够的信息。合法的 HTTPS 公网地址:确认正常业务不受影响;HTTP、非标准协议和缺少主机的地址:确认协议校验有效;回环地址、私有地址、链路本地地址和 IPv6 本地地址:确认地址分类有效;解析到受限地址的测试域名:确认域名解析后的地址也会被检查;返回重定向的测试服务:确认每次跳转都重新校验;超大响应、慢响应和多次跳转:确认资源限制有效;包含特殊字符、端口和用户名字段的 URL:确认解析逻辑没有依赖字符串拼接。测试时应在本地或专门的靶场环境中完成,使用可控的测试服务记录请求即可,不要将验证目标指向生产内网或第三方系统。六、日志与监控中的关键字段SSRF 修复不能只看接口是否返回错误,还应检查服务端日志和出口流量。建议记录请求来源、业务用户、原始 URL 的脱敏摘要、解析后的主机和端口、最终决策、拒绝原因、响应大小、耗时以及重定向次数。日志中不建议直接保存可能包含凭据的完整 URL,例如 URL 用户名、查询参数和签名字段。可以记录规范化后的主机、端口和哈希摘要,并为拒绝事件设置告警阈值。若同一用户短时间内大量提交不同内网地址、不断触发 DNS 解析失败或重复尝试重定向,应纳入异常行为分析。一个可复现的安全修复,应该同时回答三个问题:危险请求是否被拦截、正常请求是否仍然可用、拦截行为是否能够被日志和监控准确识别。七、常见错误与改进建议只拦截几个固定字符串:应改为基于标准解析结果和地址分类判断。只校验第一次请求:应关闭自动重定向,或对每次跳转重新验证。只依赖应用层过滤:应配合出站网络策略,限制服务账号的访问范围。只关注响应内容:应同时关注状态码、耗时、错误信息和业务副作用。修复后只做单个回归用例:应覆盖协议、地址族、DNS、重定向和资源限制等测试维度。总结SSRF 的本质是服务端获得了超出业务需要的网络访问能力。代码审计时,应从用户输入一路追踪到实际出网点,重点检查 URL 解析、DNS 解析、地址分类、重定向和资源控制,而不是停留在函数名层面。比较可靠的治理方式是“业务白名单限制 + 标准化解析校验 + 重定向控制 + 出站网络隔离 + 完整监控”多层配合。对于长期维护的系统,还应将这些规则封装成统一组件,并通过单元测试、集成测试和网络策略测试持续验证,避免不同业务各自实现一套不完整的 URL 过滤逻辑。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 在代 代码 码审 审计 计中 中并 并不 不少 少见 在代码 代码审 码审计 审计中 计中并 中并不 并不少 不少见 在代码审 代码审计 码审计中 审计中并 计中并不 中并不少 并不少见 它通 通常 常不 不是 是一 一个 个单 单独 独的 它通常 通常不 常不是 不是一 是一个 一个单 个单独 单独的 它通常不 通常不是 常不是一 不是一个 是一个单 一个单独 个单独的 危险 险函 函数 危险函 险函数 危险函数 问题 而是 是外 外部 部输 输入 入经 经过 而是外 是外部 外部输 部输入 输入经 入经过 而是外部 是外部输 外部输入 部输入经 输入经过 解析 网络 络请 网络请 络请求 网络请求 重定 定向 向和 和响 响应 应处 处理 理等 等多 多个 个环 环节 节后 重定向 定向和 向和响 和响应 响应处 应处理 处理等 理等多 等多个 多个环 个环节 环节后 重定向和 定向和响 向和响应 和响应处 响应处理 应处理等 处理等多 理等多个 等多个环 多个环节 个环节后 形成 成了 了服 端可 可控 控的 的请 求能 能力 形成了 成了服 了服务 务端可 端可控 可控的 控的请 的请求 请求能 求能力 形成了服 成了服务 了服务端 服务端可 务端可控 端可控的 可控的请 控的请求 的请求能 请求能力 真正 正有 有效 效的 的分 分析 真正有 正有效 有效的 效的分 的分析 真正有效 正有效的 有效的分 效的分析 不能 能只 只盯 盯着 着某 某个 不能只 能只盯 只盯着 盯着某 着某个 不能只盯 能只盯着 只盯着某 盯着某个 客户 户端 端调 调用 客户端 户端调 端调用 客户端调 户端调用 而应 应当 当沿 沿着 着数 数据 据流 流判 判断 而应当 应当沿 当沿着 沿着数 着数据 数据流 据流判 流判断 而应当沿 应当沿着 当沿着数 沿着数据 着数据流 数据流判 据流判断 谁能 能控 控制 制请 求地 地址 谁能控 能控制 控制请 制请求 请求地 求地址 谁能控制 能控制请 控制请求 制请求地 请求地址 求会 会发 发往 往哪 哪里 请求会 求会发 会发往 发往哪 往哪里 请求会发 求会发往 会发往哪 发往哪里 是否 否允 允许 许重 是否允 否允许 允许重 许重定 是否允许 否允许重 允许重定 许重定向 应是 否会 会回 回显 响应是 应是否 是否会 否会回 会回显 响应是否 应是否会 是否会回 否会回显 以及 及修 修复 复是 否覆 覆盖 盖了 了所 所有 有变 变体 以及修 及修复 修复是 复是否 是否覆 否覆盖 覆盖了 盖了所 了所有 所有变 有变体 以及修复 及修复是 修复是否 复是否覆 是否覆盖 否覆盖了 覆盖了所 盖了所有 了所有变 所有变体 题场 场景 景与 与风 风险 险边 边界 界常 常见 见业 业务 务包 包括 括图 图片 片抓 抓取 问题场 题场景 场景与 景与风 与风险 风险边 险边界 边界常 界常见 常见业 见业务 业务包 务包括 包括图 括图片 图片抓 片抓取 问题场景 题场景与 场景与风 景与风险 与风险边 风险边界 险边界常 边界常见 界常见业 常见业务 见业务包 业务包括 务包括图 包括图片 括图片抓 图片抓取 回调 远程 程文 文件 件导 导入 远程文 程文件 文件导 件导入 远程文件 程文件导 文件导入 站点 点连 连通 通性 性检 检测 测和 和第 第三 三方 方接 接口 口代 代理 站点连 点连通 连通性 通性检 性检测 检测和 测和第 和第三 第三方 三方接 方接口 接口代 口代理 站点连通 点连通性 连通性检 通性检测 性检测和 检测和第 测和第三 和第三方 第三方接 三方接口 方接口代 接口代理 例如 应用 用提 提供 供如 如下 下接 应用提 用提供 提供如 供如下 如下接 下接口 应用提供 用提供如 提供如下 供如下接 如下接口 端为 为了 了读 读取 取远 程内 内容 务端为 端为了 为了读 了读取 读取远 取远程 远程内 程内容 服务端为 务端为了 端为了读 为了读取 了读取远 读取远程 取远程内 远程内容 可能 能直 直接 接使 使用 用用 用户 户提 提交 交的 的地 址发 发起 起请 可能直 能直接 直接使 接使用 使用用 用用户 用户提 户提交 提交的 交的地 的地址 地址发 址发起 发起请 起请求 可能直接 能直接使 直接使用 接使用用 使用用户 用用户提 用户提交 户提交的 提交的地 交的地址 的地址发 地址发起 址发起请 发起请求 如果 果缺 缺少 少目
  24. 文件上传功能看似只是把一个文件保存到服务器,但它同时涉及文件名处理、内容校验、存储位置、访问控制、解析器行为和部署配置等多个边界。实际审计中,很多问题并不来自某一行明显的危险代码,而是来自“校验了一部分、信任了另一部分”的组合。本文以防守和修复验证为主线,整理一次文件上传功能的审计方法。示例使用抽象代码和本地测试思路,不涉及对真实系统的攻击操作。一、问题场景与风险边界典型上传接口通常包含以下流程:接收 multipart/form-data 请求。读取原始文件名、扩展名和 MIME 类型。检查大小、后缀或文件头。生成保存路径并写入磁盘或对象存储。返回文件 URL,供用户后续访问。风险主要集中在四个方面:文件内容被当作脚本、模板或配置解析,形成远程代码执行风险。上传文件覆盖已有文件,导致配置、缓存或业务数据被破坏。通过路径拼接影响保存位置,形成路径穿越或任意文件写入。上传后的文件缺少访问控制,导致敏感文档越权读取。审计时不要只问“扩展名是否限制”,还要问“文件最终被谁解析、保存在哪里、谁可以访问、生命周期如何管理”。二、审计的第一步:画出数据流建议先从控制器或路由入口开始,沿着数据流跟踪以下变量:原始文件名:通常来自客户端,不能作为可信路径组件。Content-Type:由客户端控制,只能作为辅助信号。文件内容:需要基于实际字节进行校验,必要时调用可靠的解析库。目标文件名:应由服务端生成,避免继承用户输入。保存目录:应来自固定配置,并在启动时校验其权限和位置。返回地址:需要经过统一的访问控制和下载策略。在代码审计中,可以优先搜索以下关键词,再反向确认调用链:getOriginalFilename getContentType transferTo Files.write FileOutputStream path.join resolve sendFile download static resource mapping搜索结果只是入口,关键是确认变量是否经过了规范化、验证和权限控制。尤其要注意同一个文件可能先被安全目录保存,之后又被异步任务移动到可执行目录。三、常见缺陷一:只校验扩展名或 MIME 类型扩展名和 MIME 类型都可以被客户端修改,不能单独作为文件类型判断依据。一个相对稳妥的策略是分层校验:业务层限制允许的文件类型和大小。服务端根据文件头或解析库判断真实类型。对图片、压缩包、文档等格式分别使用对应解析器。必要时进行转码或重新生成,避免直接信任原始内容。例如,图片上传不应只判断文件名是否以 .jpg 结尾,而应尝试使用图像库解码,并在解码成功后重新编码为服务端认可的格式。重新编码还能去除部分非必要元数据,降低解析器差异带来的风险。需要注意的是,文件头检查不是万能的。多格式文件、解析器兼容差异以及后续处理链都可能产生新的风险。因此,类型校验必须和存储隔离、下载响应头及权限控制一起设计。四、常见缺陷二:路径拼接与文件名信任下面是一个不推荐的简化示例。问题不在于使用了某个特定语言,而在于服务端直接信任用户提供的文件名:String name = upload.getOriginalFilename(); Path target = Paths.get(uploadDir, name); upload.transferTo(target.toFile());这里至少存在几个审计疑点:name 可能包含路径分隔符或特殊路径片段。同名文件可能覆盖已有内容。目标目录是否位于静态资源目录中并不明确。没有体现大小、类型和内容校验。更合理的做法是让原始文件名只作为展示信息保存,实际落盘名称由服务端生成。路径也应经过规范化并确认仍位于预期目录内:String originalName = upload.getOriginalFilename(); String extension = extensionOf(originalName); validateExtension(extension); validateSize(upload.getSize()); validateContent(upload.getInputStream(), extension); String storedName = randomIdentifier() + normalizedExtension(extension); Path base = Paths.get(configuredUploadDir).toAbsolutePath().normalize(); Path target = base.resolve(storedName).normalize(); if (!target.startsWith(base)) { throw new SecurityException(&quot;invalid storage path&quot;); } save(upload, target); recordMetadata(originalName, storedName, currentUserId());其中,randomIdentifier 应使用可靠的随机标识生成方式;normalizedExtension 只能从服务端认可的枚举中选择,而不是简单返回用户输入的字符串。五、常见缺陷三:上传目录与解析目录混用即使文件类型校验做得比较完整,也不建议把用户上传目录放在应用代码目录、模板目录、脚本目录或可执行目录下。更稳妥的存储策略包括:上传目录与应用部署目录分离。上传目录不允许脚本执行,不参与模板解析。应用进程只拥有必要的读写权限,不使用过高系统权限。对外下载通过受控接口完成,而不是直接暴露物理目录。优先使用对象存储,并关闭不必要的公共读权限。在反向代理或应用服务器配置中,也要检查静态资源映射、脚本处理规则和错误页面回退逻辑。某些部署环境会把未知扩展名交给上游解析,导致“上传目录不可执行”的设计在配置层失效。六、访问控制:上传安全不等于下载安全很多系统只关注“谁能上传”,忽略了“谁能读取”。对上传文件的访问应至少回答以下问题:文件是否属于某个用户、项目或租户?下载请求是否重新执行授权检查?文件 URL 是否包含可预测的自增编号?删除、替换和分享操作是否有独立权限?管理员、普通用户和服务账号看到的内容是否一致?推荐使用业务对象 ID 与存储对象 ID 分离的方式。前端拿到的可以是业务层标识,服务端根据当前会话查询授权关系,再映射到实际存储对象。不要把物理路径、内部桶名或未经授权的对象键直接返回给客户端。七、修复验证:用测试证明边界真的生效修复后不能只看代码差异,至少应覆盖以下测试类别:合法文件:允许的类型、大小和编码能够正常上传和下载。边界大小:刚好达到限制值、超过限制值时行为一致且可预期。伪造类型:扩展名、MIME 和实际内容不一致时应拒绝。特殊文件名:包含空格、Unicode 字符、路径分隔符和超长名称时不会影响保存路径。同名上传:不会覆盖其他用户或已有对象。越权下载:修改业务对象标识后不能读取无权限文件。异常中断:上传过程中断不会留下可被直接访问的半成品文件。在自动化测试中,可以使用临时目录和模拟存储,验证最终路径与权限结果,而不是向生产目录写入测试文件。下面是一个适合放在单元测试或集成测试中的检查思路:assert upload(validImage).status == 200 assert upload(fileWithMismatchedContent).status == 400 assert upload(fileExceedingLimit).status == 413 assert upload(nameContainingPathSeparators).storedOutsideOriginalName == true assert download(fileOwnedByOtherUser).status in [403, 404] assert storedObjectName != originalFilename此外,还应检查日志是否记录了用户、业务对象、文件大小、校验结果和失败原因,但不要把文件内容、访问令牌或敏感个人信息直接写入日志。八、代码审计中的几个容易遗漏的点异步处理链:上传接口本身安全,但消息队列消费者可能把文件移动到高风险目录。压缩包解压:需要防止解压路径越界,并限制文件数量、总大小和递归层级。缩略图生成:图像解析器和媒体处理工具应及时更新,并限制资源消耗。文件替换:更新操作不能只校验新增文件,旧对象的删除和新对象的绑定也要有权限检查。错误处理:不要把服务器绝对路径、解析器堆栈或对象存储内部信息返回给客户端。备份与临时目录:临时文件、失败文件和备份文件同样可能被静态服务暴露。九、推荐的防守清单使用服务端生成的不可预测文件名,原始文件名仅作为元数据保存。使用允许列表限制扩展名、媒体类型和文件大小。对关键格式进行真实解析或转码,不依赖单一请求头。将上传目录置于应用和脚本执行目录之外。下载时执行对象级授权检查,并设置合适的 Content-Disposition 与 Content-Type。对路径进行规范化,并确认目标路径仍位于固定根目录之下。限制上传、下载、解压和转换过程的资源消耗。使用自动化测试覆盖异常输入、越权访问和中断场景。定期更新文件解析库,并对高风险处理链进行隔离。总结文件上传漏洞的本质,是不可信数据跨越了输入、存储、解析或访问控制边界。单独增加一个后缀判断通常只能解决很小的一部分问题。完整的防护应当从数据流出发,分别确认文件内容、文件名、存储路径、解析环境和访问权限。在实际项目中,建议把上传功能当作一个独立的安全模块设计:输入阶段负责校验,存储阶段负责隔离,访问阶段负责授权,处理阶段负责资源限制,运维阶段负责日志和监控。这样即使某一层出现遗漏,也能尽量避免问题直接扩大为任意文件写入、代码执行或数据越权。 代码审计、调用链与关键函数定位示意 文件 件上 上传 传功 功能 能看 看似 似只 只是 是把 把一 一个 个文 件保 保存 存到 到服 服务 务器 文件上 件上传 上传功 传功能 功能看 能看似 看似只 似只是 只是把 是把一 把一个 一个文 个文件 文件保 件保存 保存到 存到服 到服务 服务器 文件上传 件上传功 上传功能 传功能看 功能看似 能看似只 看似只是 似只是把 只是把一 是把一个 把一个文 一个文件 个文件保 文件保存 件保存到 保存到服 存到服务 到服务器 但它 它同 同时 时涉 涉及 及文 件名 名处 处理 但它同 它同时 同时涉 时涉及 涉及文 及文件 文件名 件名处 名处理 但它同时 它同时涉 同时涉及 时涉及文 涉及文件 及文件名 文件名处 件名处理 内容 容校 校验 内容校 容校验 内容校验 存储 储位 位置 存储位 储位置 存储位置 访问 问控 控制 访问控 问控制 访问控制 解析 析器 器行 行为 为和 和部 部署 署配 配置 置等 等多 多个 个边 边界 解析器 析器行 器行为 行为和 为和部 和部署 部署配 署配置 配置等 置等多 等多个 多个边 个边界 解析器行 析器行为 器行为和 行为和部 为和部署 和部署配 部署配置 署配置等 配置等多 置等多个 等多个边 多个边界 实际 际审 审计 计中 实际审 际审计 审计中 实际审计 际审计中 很多 多问 问题 题并 并不 不来 来自 自某 某一 一行 行明 明显 显的 的危 危险 险代 代码 很多问 多问题 问题并 题并不 并不来 不来自 来自某 自某一 某一行 一行明 行明显 明显的 显的危 的危险 危险代 险代码 很多问题 多问题并 问题并不 题并不来 并不来自 不来自某 来自某一 自某一行 某一行明 一行明显 行明显的 明显的危 显的危险 的危险代 危险代码 而是 是来 而是来 是来自 而是来自 验了 了一 一部 部分 校验了 验了一 了一部 一部分 校验了一 验了一部 了一部分 信任 任了 了另 另一 信任了 任了另 了另一 另一部 信任了另 任了另一 了另一部 另一部分 的组 组合 的组合 本文 文以 以防 防守 守和 和修 修复 复验 验证 证为 为主 主线 本文以 文以防 以防守 防守和 守和修 和修复 修复验 复验证 验证为 证为主 为主线 本文以防 文以防守 以防守和 防守和修 守和修复 和修复验 修复验证 复验证为 验证为主 证为主线 整理 理一 一次 次文 能的 的审 计方 方法 整理一 理一次 一次文 次文件 功能的 能的审 的审计 审计方 计方法 整理一次 理一次文 一次文件 次文件上 传功能的 功能的审 能的审计 的审计方 审计方法 示例 例使 使用 用抽 抽象 象代 码和 和本 本地 地测 测试 试思 思路 示例使 例使用 使用抽 用抽象 抽象代 象代码 代码和 码和本 和本地 本地测 地测试 测试思 试思路 示例使用 例使用抽 使用抽象 用抽象代 抽象代码 象代码和 代码和本 码和本地 和本地测 本地测试 地测试思 测试思路 不涉 及对 对真 真实 实系 系统 统的 的攻 攻击 击操 操作 不涉及 涉及对 及对真 对真实 真实系 实系统 系统的 统的攻 的攻击 攻击操 击操作 不涉及对 涉及对真 及对真实 对真实系 真实系统 实系统的 系统的攻 统的攻击 的攻击操 攻击操作 题场 场景 景与 与风 风险 险边 界典 典型 型上 传接 接口 口通 通常 常包 包含 含以 以下 下流 流程 问题场 题场景 场景与 景与风 与风险 风险边 险边界 边界典 界典型 典型上 型上传 上传接 传接口 接口通 口通常 通常包 常包含 包含以 含以下 以下流 下流程 问题场景 题场景与 场景与风 景与风险 与风险边 风险边界 险边界典 边界典型 界典型上 典型上传 型上传接 上传接口 传接口通 接口通常 口通常包 通常包含 常包含以 包含以下 含以下流 以下流程 接收 请求 读取 取原 原始 始文 读取原 取原始 原始文 始文件 读取原始 取原始文 原始文件 始文件名 扩展 展名 名和 扩展名 展名和 扩展名和 类型 检查 查大 大小 检查大 查大小 检查大小 后缀 缀或 或文 件头 后缀或 缀或文 或文件 文件头 后缀或文 缀或文件 或文件头 生成 成保 存路 路径 径并 并写 写入 入磁 磁盘 盘或 或对 对象 象存 生成保 成保存 保存路 存路径 路径并 径并写 并写入 写入磁 入磁盘 磁盘或 盘或对 或对象 对象存 象存储 生成保存 成保存路 保存路径 存路径并 路径并写 径并写入 并写入磁 写入磁盘 入磁盘或 磁盘或对 盘或对象 或对象存 对象存储 返回 回文 返回文 回文件 返回文件 供用
  25. 背景Java 反序列化问题在很多老系统里并不罕见,尤其是历史项目中混用了多种 JSON、RPC、缓存和消息队列组件时,风险点经常不是单一漏洞,而是“危险依赖 + 不受控输入 + 宽松配置 + 缺少边界校验”的组合。 这篇文章整理一次偏防守视角的排查思路,重点放在如何识别项目中的反序列化风险、如何做代码审计、如何验证风险是否存在,以及如何给出可落地的修复建议。文中不会提供攻击载荷或外部目标利用步骤,适合作为内部安全评审、上线前检查和历史系统治理的参考。 问题场景某 Java Web 系统经历过多次迭代,早期模块使用了多种序列化方式,包括 Java 原生序列化、JSON 反序列化、Redis 缓存对象序列化、消息队列消费端对象转换等。安全排查时发现几个典型特征: pom.xml 中存在较老版本的 JSON 处理库和通用工具库。部分接口接收字符串后直接反序列化为对象。缓存层使用 JDK 默认序列化器存储复杂对象。消息消费端缺少消息来源校验和字段白名单校验。历史代码中存在 Object、Map、泛型擦除等宽泛类型接收方式。这类系统不一定马上存在可利用漏洞,但已经具备较高的审计优先级。实际工作中,我通常不会一上来就“找 payload”,而是先确认数据流、组件版本、对象边界和运行时配置。 技术分析反序列化风险的核心不是“用了某个库”这么简单,而是应用是否把不可信输入转换成了可执行语义更强的对象。常见风险入口大致有几类: HTTP 请求体:JSON、XML、表单字段中包含复杂对象结构。Cookie 或 Header:历史系统可能把用户状态序列化后放在客户端。缓存系统:Redis、Memcached 中存放 JDK 序列化对象或通用 JSON 对象。消息队列:消费者对消息体直接做对象反序列化。文件导入:上传文件被解析为对象图,例如配置导入、任务导入。RPC 框架:服务间通信中使用 Hessian、Kryo、JDK Serialize 等。审计时需要同时看“入口”和“类型控制”。如果代码只把 JSON 解析成固定 DTO,并且字段类型简单、无多态、无自动类型识别,风险相对可控;如果允许客户端指定类型,或服务端根据输入动态加载类,就需要重点关注。 依赖识别第一步建议从依赖树开始,把项目里可能参与序列化和反序列化的组件列出来。Maven 项目可以使用如下命令: mvn -q dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind mvn -q dependency:tree -Dincludes=com.alibaba:fastjson mvn -q dependency:tree -Dincludes=org.springframework:spring-core mvn -q dependency:tree -Dincludes=org.apache.commons:commons-collections4如果项目是多模块工程,建议在根目录输出完整依赖树,保存为文件后再 grep 关键组件: mvn dependency:tree -Dverbose > dependency-tree.txt cat dependency-tree.txt | grep -Ei "fastjson|jackson|xstream|hessian|kryo|snakeyaml|commons-collections|serial"Gradle 项目可以查看 runtimeClasspath: ./gradlew dependencies --configuration runtimeClasspath > dependencies.txt grep -Ei "fastjson|jackson|xstream|hessian|kryo|snakeyaml|commons-collections|serial" dependencies.txt这里的目标不是简单判断“有某依赖就有漏洞”,而是为后续代码检索提供方向。很多风险来自间接依赖,例如某个老组件引入了过期 jackson-databind,业务代码未必直接声明。 代码审计重点依赖识别之后,需要从代码层面确认哪些位置真正处理了不可信输入。可以先用关键字粗筛: grep -RIn "ObjectInputStream\|readObject\|readUnshared" src/main/java grep -RIn "JSON.parse\|parseObject\|parseArray" src/main/java grep -RIn "ObjectMapper\|readValue\|enableDefaultTyping\|activateDefaultTyping" src/main/java grep -RIn "XStream\|fromXML\|Yaml\|loadAs" src/main/java grep -RIn "RedisTemplate\|JdkSerializationRedisSerializer" src/main/java审计时我会重点记录以下信息: 输入来源:是否来自 HTTP、MQ、缓存、文件、第三方回调。目标类型:是否为固定 DTO,还是 Object、Map、接口、抽象类。类型控制:是否启用自动类型、多态类型、默认类型推断。过滤逻辑:是否有字段白名单、长度限制、签名校验、来源校验。异常处理:是否吞掉异常导致异常输入被重试或进入补偿流程。调用链位置:反序列化后对象是否进入表达式执行、模板渲染、反射调用等高风险路径。一个比较实用的判断标准:如果不可信输入能够影响“实例化什么类”或“调用什么方法”,审计优先级就应该提高。 典型风险模式下面列几个实际项目里常见的风险模式,便于对照检查。 风险模式常见表现建议处理JDK 原生反序列化ObjectInputStream 读取外部输入避免直接使用;必须使用时加入 ObjectInputFilter 和类型白名单JSON 自动类型允许输入影响类名或多态类型关闭自动类型;仅反序列化到固定 DTO宽泛类型接收Object、Map、List<Object> 接收请求体改为明确 DTO;字段做白名单校验缓存对象序列化Redis 使用 JDK 序列化复杂对象改用 JSON DTO 或专用序列化格式;限制可读写方消息体直接转换MQ 消费端信任所有消息内容增加 topic 权限、签名校验、schema 校验关键步骤:建立可复现的安全验证流程验证反序列化风险时,建议只在本地或授权测试环境中做,且验证目标应是“确认危险配置是否生效”,而不是尝试执行系统命令或控制目标。一个可控的验证方式是构造无害类,观察是否被非预期实例化。 例如在内部测试模块中定义一个只打印日志的类,用来验证某段反序列化逻辑是否允许外部输入影响类型选择。注意,这类测试代码不要带入生产环境。 public class SafeProbe { public SafeProbe() { System.out.println("SafeProbe constructed for deserialization test"); } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }如果某段解析逻辑本来只应该得到 UserDTO,却能根据输入实例化 SafeProbe,就说明类型边界存在问题。此时不需要进一步尝试危险利用,已经足够支撑风险结论。 对于 Jackson,多态配置需要重点检查。历史代码中常见如下危险配置: // 需要重点排查:历史代码中可能存在类似配置 ObjectMapper mapper = new ObjectMapper(); // enableDefaultTyping 已不建议使用,容易扩大类型反序列化边界 // mapper.enableDefaultTyping();更稳妥的方式是固定目标类型,并对 DTO 字段做校验: ObjectMapper mapper = new ObjectMapper(); UserCreateRequest req = mapper.readValue(json, UserCreateRequest.class); if (req.getUsername() == null || req.getUsername().length() > 64) { throw new IllegalArgumentException("invalid username"); }JDK ObjectInputFilter 的使用如果由于兼容性原因短期内无法移除 JDK 原生序列化,至少应该增加类型过滤。JDK 9 之后可以使用 ObjectInputFilter;JDK 8 的部分更新版本也提供了相关能力。 ObjectInputStream ois = new ObjectInputStream(inputStream); ObjectInputFilter filter = ObjectInputFilter.Config.createFilter( "com.example.dto.*;java.base/*;!*" ); ois.setObjectInputFilter(filter); Object obj = ois.readObject();过滤规则要尽量收敛,只允许业务 DTO 和必要的 JDK 基础类型。不要为了省事写成大范围通配,例如允许整个第三方包或所有类,这样基本失去了过滤价值。 Redis 序列化器治理很多老项目使用 RedisTemplate 默认或历史配置,可能把复杂对象以 JDK 序列化格式写入 Redis。这个问题有两个隐患:一是可读性差,不利于排查;二是一旦 Redis 写入边界失控,反序列化风险会被放大。 建议检查 RedisTemplate 配置,确认 key、value、hashValue 的序列化方式: @Bean public RedisTemplate<String, UserCacheDTO> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, UserCacheDTO> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }这里也要注意,JSON 序列化不等于天然安全。如果启用了过宽的类型信息,仍然可能出现问题。更推荐缓存固定结构的 DTO,避免缓存业务实体、Session 对象或包含复杂行为的对象。 消息队列消费端校验MQ 场景中,反序列化风险常常来自“默认信任内部消息”。但在真实环境里,消息可能来自多个服务、测试工具、补偿脚本,甚至历史遗留生产者。消费者应当把消息当作不可信输入处理。 建议至少做到: 按 topic 和 consumer group 收敛权限,不允许无关服务写入。消息体使用固定 schema,例如明确 DTO 字段。消费前校验版本号、来源标识、签名或内部认证信息。反序列化失败不要无限重试,避免形成异常消息堆积。对高风险字段做长度、格式、枚举值校验。public void consume(String body) throws Exception { OrderEvent event = objectMapper.readValue(body, OrderEvent.class); if (!"order-service".equals(event.getSource())) { throw new IllegalArgumentException("invalid source"); } if (event.getOrderId() == null || event.getOrderId().length() > 64) { throw new IllegalArgumentException("invalid orderId"); } handle(event); }修复建议反序列化风险的修复要按优先级推进,避免只做版本升级却留下危险调用方式。 优先移除不必要的原生反序列化入口。升级存在公开安全问题的组件版本,并确认兼容性。关闭自动类型识别、默认类型推断等宽松配置。所有外部输入反序列化到固定 DTO,不直接接收 Object。引入字段级校验,包括长度、格式、枚举、范围。缓存和 MQ 中只传输数据对象,不传输带行为的复杂对象。为必须保留的 ObjectInputStream 配置类型白名单。在 CI 中加入依赖扫描和危险 API 检索。CI 中可以加入简单的危险 API 检查,先作为提示项,不一定一开始就阻断构建: #!/bin/sh set -e TARGET="src/main/java" grep -RIn "ObjectInputStream\|readObject\|enableDefaultTyping\|activateDefaultTyping\|JdkSerializationRedisSerializer" "$TARGET" || true如果团队代码量较大,可以进一步把这些规则接入 Semgrep、CodeQL 或内部静态扫描平台,并维护误报白名单。 注意点不要只依赖 WAF 拦截。反序列化入口可能在 MQ、缓存、RPC,不一定经过 HTTP 网关。不要把“升级版本”当成唯一修复。危险配置和宽泛类型仍然可能带来风险。不要在生产环境验证危险载荷。确认类型边界失控即可证明问题。不要忽略内部系统。很多事故来自内部链路的信任假设失效。不要把实体类直接作为外部 DTO。实体类字段复杂,后续改动容易扩大攻击面。总结Java 反序列化治理更像是一项工程化工作:先识别依赖,再确认入口,然后收敛类型边界,最后通过 CI 和配置基线持续约束。单次排查可以解决已知问题,但长期看,真正有效的是把“固定 DTO、白名单类型、字段校验、最小权限”变成开发习惯。 我的经验是,审计这类问题时不要急着追求复杂利用链。只要能证明不可信输入可以影响反序列化类型,或者关键入口缺少类型和字段约束,就应该推动修复。安全评审的目标不是展示攻击技巧,而是把风险在上线前或事故前收住。 代码审计、调用链与关键函数定位示意 背景 反序 序列 列化 化问 问题 题在 在很 很多 多老 老系 系统 统里 里并 并不 不罕 罕见 反序列 序列化 列化问 化问题 问题在 题在很 在很多 很多老 多老系 老系统 系统里 统里并 里并不 并不罕 不罕见 反序列化 序列化问 列化问题 化问题在 问题在很 题在很多 在很多老 很多老系 多老系统 老系统里 系统里并 统里并不 里并不罕 并不罕见 尤其 其是 是历 历史 史项 项目 目中 中混 混用 用了 了多 多种 尤其是 其是历 是历史 历史项 史项目 项目中 目中混 中混用 混用了 用了多 了多种 尤其是历 其是历史 是历史项 历史项目 史项目中 项目中混 目中混用 中混用了 混用了多 用了多种 缓存 存和 和消 消息 息队 队列 列组 组件 件时 缓存和 存和消 和消息 消息队 息队列 队列组 列组件 组件时 缓存和消 存和消息 和消息队 消息队列 息队列组 队列组件 列组件时 风险 险点 点经 经常 常不 不是 是单 单一 一漏 漏洞 风险点 险点经 点经常 经常不 常不是 不是单 是单一 单一漏 一漏洞 风险点经 险点经常 点经常不 经常不是 常不是单 不是单一 是单一漏 单一漏洞 而是 危险 险依 依赖 危险依 险依赖 危险依赖 不受 受控 控输 输入 不受控 受控输 控输入 不受控输 受控输入 宽松 松配 配置 宽松配 松配置 宽松配置 缺少 少边 边界 界校 校验 缺少边 少边界 边界校 界校验 缺少边界 少边界校 边界校验 的组 组合 的组合 这篇 篇文 文章 章整 整理 理一 一次 次偏 偏防 防守 守视 视角 角的 的排 排查 查思 思路 这篇文 篇文章 文章整 章整理 整理一 理一次 一次偏 次偏防 偏防守 防守视 守视角 视角的 角的排 的排查 排查思 查思路 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次偏 一次偏防 次偏防守 偏防守视 防守视角 守视角的 视角的排 角的排查 的排查思 排查思路 重点 点放 放在 在如 如何 何识 识别 别项 中的 的反 化风 重点放 点放在 放在如 在如何 如何识 何识别 识别项 别项目 目中的 中的反 的反序 列化风 化风险 重点放在 点放在如 放在如何 在如何识 如何识别 何识别项 识别项目 别项目中 项目中的 目中的反 中的反序 的反序列 序列化风 列化风险 何做 做代 代码 码审 审计 如何做 何做代 做代码 代码审 码审计 如何做代 何做代码 做代码审 代码审计 何验 验证 证风 险是 是否 否存 存在 如何验 何验证 验证风 证风险 风险是 险是否 是否存 否存在 如何验证 何验证风 验证风险 证风险是 风险是否 险是否存 是否存在 以及 及如 何给 给出 出可 可落 落地 地的 的修 修复 复建 建议 以及如 及如何 如何给 何给出 给出可 出可落 可落地 落地的 地的修 的修复 修复建 复建议 以及如何 及如何给 如何给出 何给出可 给出可落 出可落地 可落地的 落地的修 地的修复 的修复建 修复建议 文中 中不 不会 会提 提供 供攻 攻击 击载 载荷 荷或 或外 外部 部目 目标 标利 利用 用步 步骤 文中不 中不会 不会提 会提供 提供攻 供攻击 攻击载 击载荷 载荷或 荷或外 或外部 外部目 部目标 目标利 标利用 利用步 用步骤 文中不会 中不会提 不会提供 会提供攻 提供攻击 供攻击载 攻击载荷 击载荷或 载荷或外 荷或外部 或外部目 外部目标 部目标利 目标利用 标利用步 利用步骤 适合 合作 作为 为内 内部 部安 安全 全评 评审 适合作 合作为 作为内 为内部 内部安 部安全 安全评 全评审 适合作为 合作为内 作为内部 为内部安 内部安全 部安全评 安全评审 上线 线前 前检 检查 查和 和历 史系 统治 治理 理的 的参 参考 上线前 线前检 前检查 检查和 查和历 和历史 历史系 史系统 系统治 统治理 治理的 理的参 的参考 上线前检 线前检查 前检查和 检查和历 查和历史 和历史系 历史系统 史系统治 系统治理 统治理的 治理的参 理的参考 题场 场景 景某 问题场 题场景 场景某 问题场景 题场景某 统经 经历 历过 过多 多次 次迭 迭代 系统经 统经历 经历过 历过多 过多次 多次迭 次迭代 系统经历 统经历过 经历过多 历过多次 过多次迭 多次迭代 早期 期模 模块 块使 使用 种序 化方 方式 早期模 期模块 模块使 块使用 使用了 多种序 种序列 列化方 化方式 早期模块 期模块使 模块使用 块使用了 使用了多 了多种序 多种序列 种序列化 序列化方 列化方式 包括 原生 生序 原生序 生序列 原生序列 生序列化 存对 对象
  26. SSRF(服务器端请求伪造)在代码审计中并不难被识别,难点通常在于判断它是否真的能够触达高风险网络资源。很多审计结果停留在“用户可控 URL 传入 HTTP 客户端”这一层,没有继续分析 URL 解析、DNS 解析、重定向、代理配置和出站网络策略,最终容易出现误报或低估风险。 本文以常见的“服务端抓取 URL”功能为例,整理一套适合代码审计和安全验证的分析方法。示例均限定在本地测试环境,不涉及对真实内网、云平台元数据服务或第三方系统的访问。 一、问题场景与审计入口常见业务包括图片代理、网页预览、Webhook 调试、PDF 转换、RSS 抓取和远程文件导入。代码形式可能不同,但通常具备以下数据流: HTTP 参数、JSON 字段或消息队列内容进入 URL 变量。URL 经过字符串拼接、模板替换或简单过滤。程序调用 HTTP 客户端发起服务端请求。响应内容被返回给用户、写入文件,或继续交给解析器处理。审计时建议先从出站请求 API 反向追踪输入来源,而不是只搜索“ssrf”关键字。Java 中常见的入口包括 HttpURLConnection、Apache HttpClient、OkHttp、Spring WebClient;Node.js 中常见的是 axios、got、fetch;Python 中则包括 requests、urllib 和 httpx。 String target = request.getParameter("url"); URL url = new URL(target); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(3000); conn.setReadTimeout(5000); InputStream body = conn.getInputStream();上面的代码存在明显的安全审计价值:用户输入直接决定了请求目标,且代码中没有体现协议限制、目标地址校验、重定向策略和响应大小控制。 二、先区分“可控请求”与“有效 SSRF”并非所有服务端请求都构成同等严重的 SSRF。审计时可以从四个维度确认实际影响: 目标可控程度:用户能否控制完整 URL,还是只能选择固定域名下的路径。网络可达性:应用所在网络是否能够访问其他网段、容器网桥或管理平面。响应可见性:响应正文、状态码、响应时间或错误信息是否会反馈给用户。副作用能力:是否支持 POST、PUT 等方法,是否会携带内部凭据,是否会跟随重定向。例如,一个只允许访问固定 CDN 域名、强制使用 GET、丢弃响应正文的图片下载功能,风险通常低于一个能够访问任意协议、跟随重定向并将响应原样返回的调试接口。 实际评级不能只依据“存在 SSRF”下结论,还要结合运行账户权限、网络分段、代理配置、容器权限和响应回显情况综合判断。 三、URL 校验中最容易出现的边界问题1. 只检查字符串前缀下面这种写法经常被用于限制域名,但它检查的是字符串,不是解析后的主机: if (!url.startsWith(" { throw new IllegalArgumentException("unsupported host"); }字符串前缀无法正确区分主机、端口、用户名字段和路径。例如合法域名后追加其他字符、加入用户信息字段,或者利用不同的 URL 解析行为,都可能造成判断结果与实际连接目标不一致。安全校验应当先使用标准库解析 URL,再分别检查协议、主机、端口和路径,避免通过字符串拼接完成判断。 2. 混淆主机名与注册域名“以 example.com 结尾”并不等于“属于 example.com”。如果业务允许访问某个组织域名,应明确采用主机名精确匹配,或使用经过验证的子域名规则。对于多级公共后缀,不能简单通过最后两段字符串判断注册域名。 URI uri = URI.create(input); String scheme = uri.getScheme(); String host = uri.getHost(); int port = uri.getPort(); if (!"https".equalsIgnoreCase(scheme) || host == null) { throw new IllegalArgumentException("invalid target"); } if (!ALLOWED_HOSTS.contains(host.toLowerCase(Locale.ROOT))) { throw new IllegalArgumentException("host is not allowed"); }如果业务确实需要允许子域名,建议使用成熟的域名解析库和明确的后缀匹配逻辑,并对国际化域名统一转为 ASCII 形式后再比较。 3. 忽略数字形式和 IPv6应用只拦截常见的点分十进制私网地址并不充分。不同语言和网络库对十六进制、八进制、整数形式 IPv4,以及 IPv4 映射 IPv6 的解析行为可能不同。IPv6 还需要考虑回环地址、本地链路地址和私有地址范围。 正确做法不是不断增加黑名单字符串,而是将主机名解析为所有地址,对每个地址执行统一的地址分类,并在建立连接前再次确认解析结果。校验逻辑至少应覆盖回环、链路本地、私有网段、保留地址和多播地址等类别。 4. 只校验第一次 DNS 解析典型风险是校验阶段解析到公网地址,真正建立连接时再次解析却得到内部地址。即使应用层逻辑正确,DNS 结果在校验和连接之间发生变化也会形成竞态。 相对稳妥的方案包括: 在受控网络中使用出站代理,由代理统一完成 DNS 解析和目标策略判断。校验后将已确认的地址用于连接,并避免客户端在连接阶段重新解析主机名。通过网络层禁止应用运行环境访问不必要的私有网段,作为应用校验之外的第二道防线。需要注意的是,单纯在应用代码中缓存 DNS 结果并不能替代网络隔离,也不能解决所有代理和连接池带来的解析差异。 四、重定向是经常被遗漏的第二跳很多 HTTP 客户端默认跟随 301、302、303 或 307 重定向。即使第一跳目标通过了校验,服务端仍可能把请求引导到另一个未经校验的地址。 防护策略有两种: 业务不需要重定向时,直接关闭自动跟随。业务必须支持重定向时,对每一跳重新执行协议、主机、端口和地址范围校验,并限制最大跳数。client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(3)) .build();审计时应继续追踪客户端封装层的默认配置,不能只看业务代码是否显式设置了重定向选项。 五、协议、端口和请求能力同样重要如果业务只是下载 HTTPS 图片,就不应该接受任意协议。除 HTTP 和 HTTPS 外,某些客户端或底层处理链还可能支持文件、本地套接字、FTP 等非预期协议。协议限制应在 URL 解析完成后进行精确比较,而不是依赖客户端“通常不会支持”。 端口也应采用白名单策略。对于固定业务,最好只允许标准 HTTPS 端口;如果确实需要自定义端口,应明确列出允许范围。与此同时,还要限制请求方法、请求头、请求体大小和认证信息转发,避免把服务端已有凭据自动带到用户指定的目标。 六、响应处理可能把 SSRF 变成二次风险服务端请求本身只是第一步。响应内容如果进入 XML、SVG、办公文档、压缩包或图像解析器,可能进一步触发解析器漏洞、资源消耗或二次请求。 建议重点检查以下位置: 是否根据响应头自动选择解析器,而没有限制真实内容类型。是否把远程响应保存到可执行目录或静态文件目录。是否没有限制响应大小,导致内存或磁盘耗尽。是否将响应中的 URL、错误信息和内部主机名原样返回给前端。是否把远程内容交给 XML、SVG、压缩包等高风险解析组件。对于文件下载功能,应设置连接超时、读取超时、最大响应体长度和整体处理时限,并采用流式读取。对于不需要展示正文的业务,可以只保留有限的状态信息,减少敏感内容回显。 七、推荐的安全实现思路一个相对完整的实现通常分为目标规范化、地址校验、连接控制和响应处理四个阶段: URI target = parseAndNormalize(userInput); requireScheme(target, Set.of("https")); requirePort(target, Set.of(443)); List<InetAddress> addresses = resolveAll(target.getHost()); for (InetAddress address : addresses) { if (isLoopback(address) || isLinkLocal(address) || isPrivate(address) || isReserved(address)) { throw new SecurityException("blocked destination"); } } HttpResponse<InputStream> response = sendWithLimits(target); validateContentType(response.headers()); copyWithMaximumSize(response.body(), MAX_BYTES);这里的代码只是结构示意,实际项目应优先使用经过维护的 URL、IP 地址和 HTTP 客户端库。尤其要确认库对代理、连接池、DNS 缓存、IPv6 和重定向的具体行为。 八、如何在本地完成安全验证验证 SSRF 时不应直接探测真实内网。可以在本机启动两个独立测试服务:一个模拟业务接口,另一个模拟“受保护资源”,仅返回固定文本并记录访问日志。测试重点放在应用是否拒绝不符合策略的目标,以及重定向、超时和大响应是否受到限制。 python3 -m http.server 18080 --bind 127.0.0.1然后在单元测试或集成测试中覆盖以下输入类别: 缺少协议、空主机、非法端口和包含用户信息的 URL。大小写混合的协议和主机名。解析后指向回环、私有地址或链路本地地址的主机名。返回重定向的测试服务。慢响应、超大响应和错误内容类型。验证记录应至少包含原始输入、规范化结果、最终解析地址、是否发生重定向、拒绝原因和耗时。日志中不要记录可能包含令牌、密码或完整查询参数的敏感 URL。 九、代码审计检查清单检查项关注点输入来源URL 是否来自用户、消息队列、数据库或可间接控制的配置。解析方式是否使用标准 URI 解析,是否存在字符串拼接和多次解析。协议限制是否只允许业务需要的协议,并拒绝空协议和非预期协议。目标限制是否校验主机、端口、解析后的全部 IP 地址。重定向是否关闭自动跟随,或对每次跳转重新校验。网络边界运行环境是否通过防火墙、代理或安全组限制出站访问。响应处理是否限制大小、超时、内容类型和解析器能力。敏感信息是否自动转发 Cookie、Authorization 或内部代理凭据。十、总结SSRF 审计的核心不是寻找一个简单的黑名单绕过点,而是还原“输入如何变成最终网络连接”的完整链路。可靠的判断应同时覆盖 URL 解析、DNS 解析、重定向、代理行为、连接地址、请求能力和响应处理。 从防守角度看,应用层校验、出站代理和网络隔离应当形成纵深防御。应用层负责明确业务允许访问什么,代理负责统一执行出站策略,网络层负责阻断不必要的内部和管理平面访问。三者结合,才能避免单个解析差异或配置疏漏导致整个边界失效。 代码审计、调用链与关键函数定位示意 服务 务器 器端 端请 请求 求伪 伪造 服务器 务器端 器端请 端请求 请求伪 求伪造 服务器端 务器端请 器端请求 端请求伪 请求伪造 在代 代码 码审 审计 计中 中并 并不 不难 难被 被识 识别 在代码 代码审 码审计 审计中 计中并 中并不 并不难 不难被 难被识 被识别 在代码审 代码审计 码审计中 审计中并 计中并不 中并不难 并不难被 不难被识 难被识别 难点 点通 通常 常在 在于 于判 判断 断它 它是 是否 否真 真的 的能 能够 够触 触达 达高 高风 风险 险网 网络 络资 资源 难点通 点通常 通常在 常在于 在于判 于判断 判断它 断它是 它是否 是否真 否真的 真的能 的能够 能够触 够触达 触达高 达高风 高风险 风险网 险网络 网络资 络资源 难点通常 点通常在 通常在于 常在于判 在于判断 于判断它 判断它是 断它是否 它是否真 是否真的 否真的能 真的能够 的能够触 能够触达 够触达高 触达高风 达高风险 高风险网 风险网络 险网络资 网络资源 很多 多审 计结 结果 果停 停留 留在 很多审 多审计 审计结 计结果 结果停 果停留 停留在 很多审计 多审计结 审计结果 计结果停 结果停留 果停留在 用户 户可 可控 用户可 户可控 用户可控 传入 客户 户端 客户端 这一 一层 这一层 没有 有继 继续 续分 分析 没有继 有继续 继续分 续分析 没有继续 有继续分 继续分析 解析 重定 定向 重定向 代理 理配 配置 置和 和出 出站 站网 络策 策略 代理配 理配置 配置和 置和出 和出站 出站网 站网络 网络策 络策略 代理配置 理配置和 配置和出 置和出站 和出站网 出站网络 站网络策 网络策略 最终 终容 容易 易出 出现 现误 误报 报或 或低 低估 估风 最终容 终容易 容易出 易出现 出现误 现误报 误报或 报或低 或低估 低估风 估风险 最终容易 终容易出 容易出现 易出现误 出现误报 现误报或 误报或低 报或低估 或低估风 低估风险 本文 文以 以常 常见 见的 本文以 文以常 以常见 常见的 本文以常 文以常见 以常见的 务端 端抓 抓取 服务端 务端抓 端抓取 服务端抓 务端抓取 功能 能为 为例 功能为 能为例 功能为例 整理 理一 一套 套适 适合 合代 计和 和安 安全 全验 验证 证的 的分 析方 方法 整理一 理一套 一套适 套适合 适合代 合代码 审计和 计和安 和安全 安全验 全验证 验证的 证的分 的分析 分析方 析方法 整理一套 理一套适 一套适合 套适合代 适合代码 合代码审 码审计和 审计和安 计和安全 和安全验 安全验证 全验证的 验证的分 证的分析 的分析方 分析方法 示例 例均 均限 限定 定在 在本 本地 地测 测试 试环 环境 示例均 例均限 均限定 限定在 定在本 在本地 本地测 地测试 测试环 试环境 示例均限 例均限定 均限定在 限定在本 定在本地 在本地测 本地测试 地测试环 测试环境 不涉 涉及 及对 对真 真实 实内 内网 不涉及 涉及对 及对真 对真实 真实内 实内网 不涉及对 涉及对真 及对真实 对真实内 真实内网 云平 平台 台元 元数 数据 据服 务或 或第 第三 三方 方系 系统 统的 的访 访问 云平台 平台元 台元数 元数据 数据服 据服务 服务或 务或第 或第三 第三方 三方系 方系统 系统的 统的访 的访问 云平台元 平台元数 台元数据 元数据服 数据服务 据服务或 服务或第 务或第三 或第三方 第三方系 三方系统 方系统的 系统的访 统的访问 问题 题场 场景 景与 与审 计入 入口 口常 见业 业务 务包 包括 括图 图片 片代 问题场 题场景 场景与 景与审 与审计 审计入 计入口 入口常 口常见 常见业 见业务 业务包 务包括 包括图 括图片 图片代 片代理 问题场景 题场景与 场景与审 景与审计 与审计入 审计入口 计入口常 入口常见 口常见业 常见业务 见业务包 业务包括 务包括图 包括图片 括图片代 图片代理 网页 页预 预览 网页预 页预览 网页预览 调试 转换 取和 和远 远程 程文 文件 件导 导入 抓取和 取和远 和远程 远程文 程文件 文件导 件导入 抓取和远 取和远程 和远程文 远程文件 程文件导 文件导入 码形 形式 式可 可能 能不 不同 代码形 码形式 形式可 式可能 可能不 能不同 代码形式 码形式可 形式可能 式可能不 可能不同 但通 常具 具备 备以 以下 下数 据流 但通常 通常具 常具备 具备以 备以下 以下数 下数据 数据流 但通常具 通常具备 常具备以 具备以下 备以下数 以下数据