跳转到帖子

所有动态

此动态墙会自动更新

  1. 昨天
  2. quentin注册了
  3. HACK1997注册了
  4. PerryWorge注册了
  5. 前几天
  6. 背景这篇文章整理的是一次比较典型的 WebShell 入侵排查过程。场景并不复杂:一台对公网开放的业务站点出现异常外连,随后在 Web 目录中发现可疑脚本文件。这里不讨论攻击利用细节,也不提供可直接用于入侵的操作,只记录防守侧如何从日志、文件、进程和代码审计几个角度把事件闭环。 类似事件在中小型业务系统里很常见,尤其是历史包袱较重的 PHP、Java Web 项目:上传目录权限过大、组件版本长期不更新、日志留存不足、应用和系统权限混用,都会放大排查难度。 初始现象最早的告警来自出口流量监控:业务服务器在非业务时间段向多个陌生 IP 发起 HTTP 请求,频率不高,但持续时间较长。同时,Nginx access.log 中出现一些访问冷门路径的请求,状态码集中在 200 和 404。 现场的几个现象比较有价值: Web 根目录下出现近期修改的脚本文件,文件名伪装成缓存文件或图片处理文件。上传目录中混有非图片后缀文件,并且存在部分双后缀文件。access.log 中有少量 POST 请求的响应体大小异常稳定。PHP-FPM 进程用户具备较大的目录写权限。这类线索不能单独定性,但可以帮助缩小范围:先确认是否存在 WebShell,再倒查入口点。 排查思路我通常会把这类事件拆成四条线并行推进: 文件线:查找近期新增、修改、权限异常、内容混淆的文件。日志线:从可疑文件访问记录向前追溯上传、编辑、解压、插件安装等行为。进程线:确认 Web 进程是否启动异常子进程或访问异常网络目标。代码线:审计入口功能,包括上传、模板编辑、文件包含、反序列化、后台插件等。这样做的好处是避免只删除 WebShell 就结束。WebShell 是结果,不是根因;如果入口漏洞还在,后续大概率会再次出现。 文件侧检查首先保全现场,不建议直接删除文件。至少先记录路径、哈希、时间戳、权限、属主和访问日志片段。可以在只读备份后再分析。 find /var/www/html -type f -mtime -7 -printf '%TY-%Tm-%Td %TT %u %g %m %p\n' | sort find /var/www/html -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.asp' -o -name '*.aspx' \) \ -printf '%s %TY-%Tm-%Td %TT %p\n' | sort -n对 PHP 项目,可以重点关注以下特征,但不要只依赖关键字命中。现在很多样本会做简单混淆,或者藏在正常业务文件里。 高风险函数:eval、assert、system、exec、shell_exec、passthru、proc_open、popen。编码混淆:base64_decode、gzinflate、str_rot13、chr 拼接、变量函数调用。异常入口:读取请求参数后直接进入动态执行、文件写入或包含。文件伪装:图片目录中的脚本文件、缓存目录中的可执行文件、异常长文件名。grep -RIn --include='*.php' -E "eval\s*\(|assert\s*\(|base64_decode\s*\(|gzinflate\s*\(|shell_exec\s*\(|passthru\s*\(" /var/www/html实际排查时,grep 只能作为第一轮筛选。很多框架、插件也会合法使用部分函数,所以需要结合文件路径、修改时间、代码上下文和访问日志判断。 日志侧关联确认可疑文件后,下一步是看它是否被访问,以及首次访问时间。以 Nginx 为例,可以按文件名检索: grep 'suspect.php' /var/log/nginx/access.log* grep 'POST' /var/log/nginx/access.log | awk '{print $1,$4,$6,$7,$9,$10}' | sort | uniq -c | sort -nr | head如果可疑文件位于上传目录,需要继续向前查同一 IP、同一会话、同一用户在首次访问前的行为。重点看这些功能: 文件上传接口:头像、附件、富文本编辑器、导入导出功能。后台模板编辑:主题、插件、页面片段可编辑功能。压缩包处理:上传 zip 后自动解压,可能带来目录穿越或脚本落地风险。远程图片抓取:服务端根据用户提供 URL 拉取资源,可能导致任意文件写入或 SSRF 相关问题。在一次真实排查中,可疑脚本首次访问之前,同一源 IP 对富文本编辑器上传接口进行了多次探测,请求路径变化不大,但文件名和 Content-Type 不断变化。虽然攻击链细节不展开,但这足以说明入口很可能在上传校验链路。 进程与网络检查如果 WebShell 已被执行,可能会留下子进程、临时文件、异常外连。这里的目标不是“反制”,而是确认影响范围。 ps -ef --forest | grep -E 'nginx|php-fpm|apache|httpd|java' ss -antp | grep -E 'php-fpm|nginx|apache|httpd|java' lsof -u www-data 2>/dev/null | head -100关注点包括: Web 进程是否启动 shell、下载工具、解释器、压缩工具等异常子进程。是否存在由 Web 用户创建的计划任务。/tmp、/var/tmp、上传目录中是否存在近期生成的二进制或脚本文件。外连目标是否与业务域名、对象存储、消息队列等合法依赖无关。crontab -u www-data -l 2>/dev/null find /tmp /var/tmp -type f -mtime -7 -ls 2>/dev/null代码审计重点:上传链路这次事件中,根因最终落在文件上传校验不足。很多上传漏洞并不是单点错误,而是多个“小问题”叠加: 只校验前端后缀,后端信任前端传来的文件名。后端只检查 Content-Type,没有基于文件内容做二次判断。上传目录位于 Web 可访问路径下,并允许脚本执行。文件名未重命名,或保留用户可控后缀。Nginx、Apache 或 PHP-FPM 对特殊后缀解析规则配置不严谨。一个更稳妥的上传处理方式应满足几个原则:白名单后缀、随机文件名、内容识别、存储隔离、禁止执行、权限最小化。示例配置如下,核心是上传目录不应执行脚本: location ^~ /uploads/ { autoindex off; types { } default_type application/octet-stream; location ~* \.(php|php5|phtml|jsp|jspx|asp|aspx|sh|pl|py)$ { return 403; } }如果是 PHP-FPM,还要确认不会把非 PHP 文件错误转交给解释器。常见建议是限制 SCRIPT_FILENAME 映射,并关闭不必要的路径信息解析: cgi.fix_pathinfo=0代码侧也要避免直接使用用户文件名。可以按业务类型生成不可预测文件名,并将原始文件名只作为元数据保存。 // 示例:上传后生成随机文件名,避免沿用用户传入名称 $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); $allow = ['jpg', 'jpeg', 'png', 'gif', 'pdf']; if (!in_array($ext, $allow, true)) { throw new RuntimeException('unsupported file type'); } $newName = bin2hex(random_bytes(16)) . '.' . $ext; $target = $uploadDir . '/' . $newName;处置步骤建议按“止血、取证、清理、修复、监控”的顺序处理,避免一上来就删文件导致证据断裂。 隔离:必要时先下线受影响实例,或在负载均衡层摘除节点。保全:打包 Web 目录、日志、进程列表、网络连接、计划任务、关键配置。确认:对可疑文件计算哈希,分析调用关系和访问记录。清理:删除 WebShell、临时文件、异常账号、异常计划任务。修复:修补上传、文件写入、模板编辑等入口漏洞,升级存在风险的组件。加固:收敛目录权限,上传目录禁止执行,Web 用户禁止登录,敏感配置移出 Web 根目录。监控:增加文件完整性监控、异常 POST 监控、出口访问监控。权限方面,比较推荐让 Web 进程只拥有必要目录的写权限,而不是整个站点目录可写。配置文件、代码目录、模板目录默认只读,上传和缓存目录单独授权。 chown -R root:root /var/www/html chown -R www-data:www-data /var/www/html/uploads /var/www/html/runtime find /var/www/html -type d -exec chmod 755 {} \; find /var/www/html -type f -exec chmod 644 {} \;常见误区只删除 WebShell,不查入口。这样通常只是把再次入侵的时间推迟几天。只看可疑关键字,不看业务上下文。误报会很多,也容易漏掉混淆样本。忽略日志留存。很多站点 access.log 只保留一两天,真正排查时已经断链。让上传目录可执行。上传目录只负责存储,不应该承担脚本运行能力。把系统用户、数据库账号、对象存储密钥长期放在 Web 可读配置中。长期建议如果团队人手有限,优先做几件收益高的事情: 保留至少 30 天 Web 访问日志,并统一到日志平台检索。对 Web 根目录做文件完整性监控,重点关注新增脚本文件。上传目录、缓存目录禁止脚本执行。应用依赖纳入版本管理,定期清理历史插件和废弃接口。生产环境关闭调试模式,后台入口增加访问控制和强认证。出口流量做基础白名单或异常告警,至少能发现非业务外连。总结WebShell 事件排查的关键不在于识别某一个样本,而在于把“文件落地、访问执行、入口漏洞、权限边界、后续动作”串起来。文件、日志、进程、代码四条线能互相印证,最终才能判断影响范围并完成修复。 从经验看,上传目录可执行、组件长期不更新、权限过大,是这类事件最常见的基础问题。把这些基础面收紧,往往比堆更多工具更有效。 代码审计、调用链与关键函数定位示意 背景 景这 这篇 篇文 文章 章整 整理 理的 的是 是一 一次 次比 比较 较典 典型 型的 背景这 景这篇 这篇文 篇文章 文章整 章整理 整理的 理的是 的是一 是一次 一次比 次比较 比较典 较典型 典型的 背景这篇 景这篇文 这篇文章 篇文章整 文章整理 章整理的 整理的是 理的是一 的是一次 是一次比 一次比较 次比较典 比较典型 较典型的 入侵 侵排 排查 查过 过程 入侵排 侵排查 排查过 查过程 入侵排查 侵排查过 排查过程 场景 景并 并不 不复 复杂 场景并 景并不 并不复 不复杂 场景并不 景并不复 并不复杂 一台 台对 对公 公网 网开 开放 放的 的业 业务 务站 站点 点出 出现 现异 异常 常外 外连 一台对 台对公 对公网 公网开 网开放 开放的 放的业 的业务 业务站 务站点 站点出 点出现 出现异 现异常 异常外 常外连 一台对公 台对公网 对公网开 公网开放 网开放的 开放的业 放的业务 的业务站 业务站点 务站点出 站点出现 点出现异 出现异常 现异常外 异常外连 随后 后在 随后在 目录 录中 中发 发现 现可 可疑 疑脚 脚本 本文 文件 目录中 录中发 中发现 发现可 现可疑 可疑脚 疑脚本 脚本文 本文件 目录中发 录中发现 中发现可 发现可疑 现可疑脚 可疑脚本 疑脚本文 脚本文件 这里 里不 不讨 讨论 论攻 攻击 击利 利用 用细 细节 这里不 里不讨 不讨论 讨论攻 论攻击 攻击利 击利用 利用细 用细节 这里不讨 里不讨论 不讨论攻 讨论攻击 论攻击利 攻击利用 击利用细 利用细节 也不 不提 提供 供可 可直 直接 接用 用于 于入 侵的 的操 操作 也不提 不提供 提供可 供可直 可直接 直接用 接用于 用于入 于入侵 入侵的 侵的操 的操作 也不提供 不提供可 提供可直 供可直接 可直接用 直接用于 接用于入 用于入侵 于入侵的 入侵的操 侵的操作 只记 记录 录防 防守 守侧 侧如 如何 何从 从日 日志 只记录 记录防 录防守 防守侧 守侧如 侧如何 如何从 何从日 从日志 只记录防 记录防守 录防守侧 防守侧如 守侧如何 侧如何从 如何从日 何从日志 进程 程和 和代 代码 码审 审计 计几 几个 个角 角度 度把 把事 事件 件闭 闭环 进程和 程和代 和代码 代码审 码审计 审计几 计几个 几个角 个角度 角度把 度把事 把事件 事件闭 件闭环 进程和代 程和代码 和代码审 代码审计 码审计几 审计几个 计几个角 几个角度 个角度把 角度把事 度把事件 把事件闭 事件闭环 类似 似事 件在 在中 中小 小型 型业 务系 系统 统里 里很 很常 常见 类似事 似事件 事件在 件在中 在中小 中小型 小型业 型业务 业务系 务系统 系统里 统里很 里很常 很常见 类似事件 似事件在 事件在中 件在中小 在中小型 中小型业 小型业务 型业务系 业务系统 务系统里 系统里很 统里很常 里很常见 尤其 其是 是历 历史 史包 包袱 袱较 较重 重的 尤其是 其是历 是历史 历史包 史包袱 包袱较 袱较重 较重的 尤其是历 其是历史 是历史包 历史包袱 史包袱较 包袱较重 袱较重的 项目 上传 传目 录权 权限 限过 过大 上传目 传目录 目录权 录权限 权限过 限过大 上传目录 传目录权 目录权限 录权限过 权限过大 组件 件版 版本 本长 长期 期不 不更 更新 组件版 件版本 版本长 本长期 长期不 期不更 不更新 组件版本 件版本长 版本长期 本长期不 长期不更 期不更新 志留 留存 存不 不足 日志留 志留存 留存不 存不足 日志留存 志留存不 留存不足 应用 用和 和系 统权 限混 混用 应用和 用和系 和系统 系统权 统权限 权限混 限混用 应用和系 用和系统 和系统权 系统权限 统权限混 权限混用 都会 会放 放大 大排 查难 难度 都会放 会放大 放大排 大排查 排查难 查难度 都会放大 会放大排 放大排查 大排查难 排查难度 初始 始现 现象 象最 最早 早的 的告 告警 警来 来自 自出 出口 口流 流量 量监 监控 初始现 始现象 现象最 象最早 最早的 早的告 的告警 告警来 警来自 来自出 自出口 出口流 口流量 流量监 量监控 初始现象 始现象最 现象最早 象最早的 最早的告 早的告警 的告警来 告警来自 警来自出 来自出口 自出口流 出口流量 口流量监 流量监控 务服 服务 务器 器在 在非 非业 务时 时间 间段 段向 向多 多个 个陌 陌生 业务服 务服务 服务器 务器在 器在非 在非业 非业务 业务时 务时间 时间段 间段向 段向多 向多个
  7. 背景最近帮团队梳理了一批内网 Web 资产,目标不是做“打点”,而是从日常运维和代码审计角度确认几个高频风险:默认配置暴露、调试接口未关闭、反向代理规则过宽、管理后台缺少访问控制、日志中存在敏感信息。复盘下来发现,真正造成风险的往往不是单个高危漏洞,而是多个弱配置叠加后的攻击面扩大。这篇文章整理一套偏防守视角的检查方法,适合安全自查、上线前验收、内部红队演练后的整改复盘。文中示例均以授权环境和本地测试为前提,不涉及未授权攻击或恶意利用。典型场景这类问题常见于以下几种环境:业务快速迭代,测试环境配置被复制到生产环境。Nginx、Apache、Tomcat、Spring Boot Admin 等组件上线后未做最小化暴露。内网服务默认认为“只在内网就安全”,缺少身份认证和访问控制。日志、备份文件、构建产物长期堆积在 Web 根目录。反向代理统一入口配置复杂,存在路径穿透、接口误暴露等问题。经验上看,内网 Web 风险的核心不是“有没有漏洞”,而是“资产是否被清楚管理、边界是否可控、敏感接口是否默认拒绝”。审计思路我一般按“资产识别 → 暴露面确认 → 配置审计 → 代码与接口核验 → 日志与凭据检查 → 加固验证”这条线推进。这样做的好处是不会一开始就陷入单点漏洞验证,而是先把整体风险面收敛清楚。一、资产识别与服务指纹整理内网资产经常存在登记不全的问题,先做基础盘点很重要。这里不建议上来就高并发扫描,容易影响业务。可以从 CMDB、网关配置、DNS 记录、Kubernetes Ingress、Nginx 配置仓库等来源交叉比对。如果是授权自查环境,可以对指定网段做低速探测,重点识别 HTTP/HTTPS 服务、标题、Server 头、证书信息和常见管理端口。nmap -sS -sV -T2 --top-ports 1000 10.10.20.0/24 -oA web_asset_scan对于 Web 标题和响应头,可以用简单脚本或内部资产平台定期采集。需要关注的信息包括:HTTP 状态码:是否存在大量 200/302 的未知服务。Server 与 X-Powered-By:是否暴露组件版本。页面标题:是否出现 Jenkins、Harbor、Grafana、Kibana、Swagger、Actuator 等管理或调试组件。证书 CN/SAN:是否泄露内部域名、项目名、环境名。二、常见弱配置检查点弱配置审计建议用清单化方式做,避免只靠个人经验。下面是我在内网 Web 审计中常看的几类。1. 目录浏览与静态文件泄露目录浏览开启后,备份包、日志、源码压缩包、SQL 文件很容易被直接下载。Nginx 中尤其要注意 autoindex 配置。# 风险配置示例,不建议在生产环境开启 autoindex on; autoindex_exact_size off; autoindex_localtime on;整改建议是生产环境关闭目录浏览,并对上传目录、日志目录、备份目录进行访问隔离。location /backup/ { deny all; return 403; } location ~* \\.(sql|bak|zip|tar|gz|log|conf)$ { deny all; return 403; }2. 调试接口暴露Spring Boot Actuator、Swagger UI、Druid Console、Jolokia 等接口在内网很常见。问题不在于这些组件本身,而是上线后缺少认证和访问控制。以 Spring Boot Actuator 为例,应避免默认暴露过多端点。生产环境建议只开放必要健康检查,并放到独立内网路径或服务发现体系里。management.endpoints.web.exposure.include=health,info management.endpoint.health.show-details=never management.server.port=9001如果必须暴露详细监控信息,建议增加网关鉴权、IP 白名单和独立认证,不要直接挂在公网或办公网可访问位置。3. 反向代理路径规则过宽Nginx 反向代理里常见的问题是 location 匹配范围过大,把内部管理接口一并代理出去。例如将根路径全部转发到后端管理服务,导致 /admin、/actuator、/swagger-ui 被一并暴露。location / { proxy_pass /> }更稳妥的方式是明确业务路径,只放行需要的 API,并对敏感路径前置拒绝规则。location ~* ^/(actuator|swagger|swagger-ui|druid|admin) { return 403; } location /api/ { proxy_pass /> }同时要注意 Nginx location 匹配优先级,尤其是普通前缀、正则匹配、^~ 的组合,很多误暴露就是规则顺序导致的。4. 默认口令与弱认证内网系统常见默认账号包括运维平台、CI/CD、数据库管理 Web 控制台、消息队列控制台等。审计时不建议做暴力破解,应优先检查配置仓库、部署文档、初始化脚本和密钥管理平台。如果发现默认口令,应按流程整改:禁用默认账号、强制改密、开启 MFA、收敛访问来源、审计历史登录记录。这里的重点是确认是否已经被使用过,而不只是改掉密码。三、代码审计中的高频问题配置问题解决后,还需要看业务代码里是否存在可被弱配置放大的风险。以下几个点在 Java/PHP/Node 项目中都比较常见。1. 文件下载接口缺少路径约束典型问题是直接使用用户输入拼接文件路径。即使外层网关限制了访问路径,内部接口一旦被误暴露,就可能读取非预期文件。// 风险写法示例 String fileName = request.getParameter(\"file\"); File file = new File(baseDir + \"/\" + fileName);较好的做法是使用文件 ID 映射真实路径,并做规范化路径校验。Path basePath = Paths.get(baseDir).toRealPath(); Path targetPath = basePath.resolve(fileName).normalize(); if (!targetPath.startsWith(basePath)) { throw new SecurityException(\"invalid file path\"); }2. 后台接口只依赖前端隐藏有些管理接口只是前端菜单不展示,后端没有权限校验。审计时可以从路由、Controller 注解、中间件配置入手,确认每个敏感操作是否有服务端鉴权。用户管理、角色管理、配置修改、任务执行、文件上传下载等接口必须做权限判断。不要只依赖 Referer、前端路由、按钮隐藏来控制权限。权限校验逻辑应集中实现,避免散落在各个业务函数中。3. 日志打印敏感字段很多泄露并不来自漏洞利用,而是日志系统中长期保存了 token、cookie、手机号、身份证、数据库连接串等信息。建议在日志组件层面统一脱敏。password=****** token=****** authorization=****** set-cookie=******同时要检查异常堆栈是否直接返回给前端。生产环境不应返回详细堆栈、绝对路径、SQL 语句和内部服务地址。四、验证与整改闭环审计价值不在于列问题,而在于闭环。一次完整整改至少应包括以下内容:确认影响范围:涉及哪些域名、路径、服务、环境。评估风险等级:是否可未授权访问、是否涉及敏感数据、是否可影响生产。制定修复方案:配置修复、代码修复、网络隔离、认证加固。回归验证:确认漏洞点不可复现,且业务功能未受影响。补充监控:对敏感路径、异常状态码、异常下载量做告警。对于 Nginx 这类入口服务,修复后建议做配置检测和灰度发布。nginx -t systemctl reload nginx对于 Java 应用,建议将安全配置纳入不同环境的配置基线,避免测试环境配置被带入生产。五、一些容易忽略的注意点不要只查公网域名,内网 DNS、Ingress、NodePort、临时域名同样重要。不要只看 200 响应,302 跳转到登录页也可能说明管理后台已暴露。不要忽略 OPTIONS、PUT、DELETE 等 HTTP 方法,错误开放可能引入额外风险。不要把办公网视为可信网络,办公终端失陷后内网 Web 资产会成为横向移动入口。不要在整改时简单加一层 Basic Auth 就结束,还需要确认鉴权、日志、访问边界是否一致。参考检查表检查项风险表现整改建议目录浏览可列出备份、日志、源码包关闭 autoindex,限制敏感后缀访问调试接口Actuator、Swagger、Druid 暴露最小化开放,增加认证和白名单反向代理敏感路径被统一代理明确 location 规则,敏感路径默认拒绝默认账号管理平台存在初始口令禁用默认账号,强制改密,开启 MFA日志泄露日志中存在 token、cookie、连接串统一脱敏,限制日志访问权限文件接口路径拼接导致越权读取使用文件 ID 映射,做规范化路径校验总结内网 Web 资产的安全问题,很多时候不是复杂漏洞,而是默认配置、历史遗留和权限边界不清造成的。审计时建议先做资产梳理,再按配置、接口、代码、日志逐层检查,最后形成可验证的整改闭环。长期来看,最有效的做法是把这些检查点固化到上线流程和配置基线里:新服务上线前必须确认暴露路径、认证方式、日志脱敏、敏感接口访问控制;已有资产定期巡检,并将结果同步给研发和运维。这样比事后临时补洞更稳,也更符合真实业务环境的安全建设节奏。 代码审计、调用链与关键函数定位示意 背景 景最 最近 近帮 帮团 团队 队梳 梳理 理了 了一 一批 批内 内网 背景最 景最近 最近帮 近帮团 帮团队 团队梳 队梳理 梳理了 理了一 了一批 一批内 批内网 背景最近 景最近帮 最近帮团 近帮团队 帮团队梳 团队梳理 队梳理了 梳理了一 理了一批 了一批内 一批内网 资产 目标 标不 不是 是做 目标不 标不是 不是做 目标不是 标不是做 打点 而是 是从 从日 日常 常运 运维 维和 和代 代码 码审 审计 计角 角度 度确 确认 认几 几个 个高 高频 频风 风险 而是从 是从日 从日常 日常运 常运维 运维和 维和代 和代码 代码审 码审计 审计角 计角度 角度确 度确认 确认几 认几个 几个高 个高频 高频风 频风险 而是从日 是从日常 从日常运 日常运维 常运维和 运维和代 维和代码 和代码审 代码审计 码审计角 审计角度 计角度确 角度确认 度确认几 确认几个 认几个高 几个高频 个高频风 高频风险 默认 认配 配置 置暴 暴露 默认配 认配置 配置暴 置暴露 默认配置 认配置暴 配置暴露 调试 试接 接口 口未 未关 关闭 调试接 试接口 接口未 口未关 未关闭 调试接口 试接口未 接口未关 口未关闭 反向 向代 代理 理规 规则 则过 过宽 反向代 向代理 代理规 理规则 规则过 则过宽 反向代理 向代理规 代理规则 理规则过 规则过宽 管理 理后 后台 台缺 缺少 少访 访问 问控 控制 管理后 理后台 后台缺 台缺少 缺少访 少访问 访问控 问控制 管理后台 理后台缺 后台缺少 台缺少访 缺少访问 少访问控 访问控制 日志 志中 中存 存在 在敏 敏感 感信 信息 日志中 志中存 中存在 存在敏 在敏感 敏感信 感信息 日志中存 志中存在 中存在敏 存在敏感 在敏感信 敏感信息 复盘 盘下 下来 来发 发现 复盘下 盘下来 下来发 来发现 复盘下来 盘下来发 下来发现 真正 正造 造成 成风 险的 的往 往往 往不 是单 单个 高危 危漏 漏洞 真正造 正造成 造成风 成风险 风险的 险的往 的往往 往往不 往不是 不是单 是单个 单个高 个高危 高危漏 危漏洞 真正造成 正造成风 造成风险 成风险的 风险的往 险的往往 的往往不 往往不是 往不是单 不是单个 是单个高 单个高危 个高危漏 高危漏洞 是多 多个 个弱 弱配 置叠 叠加 加后 后的 的攻 攻击 击面 面扩 扩大 而是多 是多个 多个弱 个弱配 弱配置 配置叠 置叠加 叠加后 加后的 后的攻 的攻击 攻击面 击面扩 面扩大 而是多个 是多个弱 多个弱配 个弱配置 弱配置叠 配置叠加 置叠加后 叠加后的 加后的攻 后的攻击 的攻击面 攻击面扩 击面扩大 这篇 篇文 文章 章整 整理 理一 一套 套偏 偏防 防守 守视 视角 角的 的检 检查 查方 方法 这篇文 篇文章 文章整 章整理 整理一 理一套 一套偏 套偏防 偏防守 防守视 守视角 视角的 角的检 的检查 检查方 查方法 这篇文章 篇文章整 文章整理 章整理一 整理一套 理一套偏 一套偏防 套偏防守 偏防守视 防守视角 守视角的 视角的检 角的检查 的检查方 检查方法 适合 合安 安全 全自 自查 适合安 合安全 安全自 全自查 适合安全 合安全自 安全自查 上线 线前 前验 验收 上线前 线前验 前验收 上线前验 线前验收 内部 部红 红队 队演 演练 练后 的整 整改 改复 内部红 部红队 红队演 队演练 演练后 练后的 后的整 的整改 整改复 改复盘 内部红队 部红队演 红队演练 队演练后 演练后的 练后的整 后的整改 的整改复 整改复盘 文中 中示 示例 例均 均以 以授 授权 权环 环境 境和 和本 本地 地测 测试 试为 为前 前提 文中示 中示例 示例均 例均以 均以授 以授权 授权环 权环境 环境和 境和本 和本地 本地测 地测试 测试为 试为前 为前提 文中示例 中示例均 示例均以 例均以授 均以授权 以授权环 授权环境 权环境和 环境和本 境和本地 和本地测 本地测试 地测试为 测试为前 试为前提 不涉 涉及 及未 未授 权攻 击或 或恶 恶意 意利 利用 不涉及 涉及未 及未授 未授权 授权攻 权攻击 攻击或 击或恶 或恶意 恶意利 意利用 不涉及未 涉及未授 及未授权 未授权攻 授权攻击 权攻击或 攻击或恶 击或恶意 或恶意利 恶意利用 典型 型场 场景 景这 这类 类问 问题 题常 常见 见于 于以 以下 下几 几种 种环 典型场 型场景 场景这 景这类 这类问 类问题 问题常 题常见 常见于 见于以 于以下 以下几 下几种 几种环 种环境 典型场景 型场景这 场景这类
  8. 背景SSRF(Server-Side Request Forgery)这几年在真实业务里出现频率一直不低,尤其是带有“URL 预览、图片抓取、Webhook、在线导入、PDF 渲染、远程文件同步”等能力的系统。它的问题不在于某个请求函数本身危险,而是服务端替用户访问了不可信 URL,并且访问环境往往比普通用户浏览器更敏感。 这篇文章整理一次较典型的 SSRF 排查思路,重点放在代码审计、复现验证和修复策略上。内容只讨论授权测试环境中的分析方法,不涉及未授权利用、内网探测或攻击扩展。 场景描述某业务提供“远程图片导入”功能,用户提交一个图片 URL,后端下载图片并存入对象存储。功能看起来很常见,入口大概类似: POST /api/image/import Content-Type: application/json { "url": " }后端逻辑包括三步: 校验 URL 字符串格式。使用 HTTP 客户端请求该 URL。判断响应 Content-Type 和文件大小后保存。问题出在第一步和第二步之间:代码只判断了 URL 是否以 http 或 https 开头,没有对解析后的主机、重定向、DNS 解析结果做约束。 问题代码示例下面是一个抽象后的示例,语言以 Java 为例,其他语言中的问题本质相同: public byte[] fetchImage(String url) throws IOException { if (!url.startsWith(" && !url.startsWith(" { throw new IllegalArgumentException("invalid scheme"); } HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setConnectTimeout(3000); conn.setReadTimeout(5000); conn.setInstanceFollowRedirects(true); String contentType = conn.getContentType(); if (contentType == null || !contentType.startsWith("image/")) { throw new IllegalArgumentException("not image"); } return conn.getInputStream().readAllBytes(); }这段代码有几个常见隐患: 只做字符串前缀判断,未使用标准 URL 解析结果进行校验。默认跟随重定向,初始 URL 安全不代表跳转后的 URL 安全。只校验 Content-Type,无法约束服务端实际访问目标。没有限制解析出的 IP 范围,例如本地地址、内网地址、链路本地地址等。没有响应体大小上限,存在资源消耗风险。技术分析SSRF 排查时,不建议只盯着“能不能访问某个地址”,更重要的是梳理服务端请求链路。一个安全的远程抓取功能,至少要明确以下几个点: 允许哪些协议:通常只允许 http 和 https。允许访问哪些域名:优先白名单,而不是黑名单。是否允许跳转:如果允许,跳转后的每一跳都要重新校验。DNS 解析结果是否可信:需要检查解析后的 IP 是否落在禁止访问的网段。是否存在 DNS Rebinding:校验和实际连接之间不能完全割裂。请求方法、请求头、超时、最大响应大小是否受控。其中最容易被忽略的是重定向和 DNS。很多修复只检查用户输入的第一跳 URL,但 HTTP 客户端自动跟随 302 后,真正访问的可能已经不是最初校验过的目标。 安全复现思路在授权测试环境中,可以搭建两个本地服务来模拟“正常外部地址跳转到受限地址”的情况。这里不提供针对真实内网资产的利用步骤,只用于验证业务代码是否会跟随跳转、是否对跳转目标重新校验。 # 启动一个普通测试服务,返回 302 跳转 python3 -m http.server 8000也可以用一个最小 Flask 服务模拟响应: from flask import Flask, redirect app = Flask(__name__) @app.route('/redirect') def r(): return redirect(' code=302) app.run(host='0.0.0.0', port=8000)另一个端口作为受限目标模拟服务: from flask import Flask app = Flask(__name__) @app.route('/test') def test(): return 'local service', 200, {'Content-Type': 'text/plain'} app.run(host='127.0.0.1', port=9000)如果业务后端请求了第一个地址,并且第二个本地服务收到请求,就说明当前代码在重定向场景下存在访问边界失控问题。实际验证时应只在本机、测试容器或授权环境内进行。 关键审计点代码审计时可以从“输入点”和“请求点”两个方向找。 1. 常见输入点图片、头像、附件的远程导入。URL 预览、链接解析、网页截图。Webhook 回调地址配置。第三方数据源导入。PDF/HTML 渲染中的远程资源加载。XML、Markdown、富文本解析中的外部资源引用。2. 常见请求函数Java:HttpURLConnection、Apache HttpClient、OkHttp、RestTemplate、WebClient。Go:net/http、http.Client。Python:requests、urllib、aiohttp。Node.js:axios、node-fetch、request、got。PHP:curl、file_get_contents、Guzzle。审计时建议全文检索这些请求库,再反向追踪 URL 参数来源。如果 URL 可被用户直接或间接控制,就需要继续确认是否有完整的目标校验。 修复思路SSRF 的修复不要依赖单点判断,建议做成统一的安全请求组件,业务层禁止直接调用通用 HTTP 客户端访问用户传入 URL。 一个相对稳妥的策略如下: 只允许 http 和 https。优先使用域名白名单,确实无法白名单时再做严格网段限制。禁止访问本地地址、私有地址、链路本地地址、保留地址、组播地址等。关闭自动重定向,手动处理跳转,并对每一跳重复校验。限制最大跳转次数,例如不超过 3 次。设置连接超时、读取超时、最大响应体大小。限制请求方法和请求头,避免带入内部凭据。下载类功能先落临时文件,再做类型和大小校验。IP 范围校验示例下面示例只展示思路,生产环境建议使用成熟 IP/CIDR 库,避免 IPv6、IPv4 映射地址、编码变体等细节处理不完整。 private static boolean isBlockedAddress(InetAddress address) { return address.isAnyLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress() || address.isMulticastAddress(); }仅使用 isSiteLocalAddress 并不够,一些保留网段、特殊地址段、IPv6 场景还需要额外覆盖。实际工程里更建议维护一组禁止 CIDR,并统一做判断。 重定向处理示例重点是不要让 HTTP 客户端自动跟随跳转,而是在每一跳前做 URL 和解析结果校验: int maxRedirects = 3; URL current = new URL(inputUrl); for (int i = 0; i 背景 这几 几年 年在 在真 真实 实业 业务 务里 里出 出现 现频 频率 率一 一直 直不 不低 这几年 几年在 年在真 在真实 真实业 实业务 业务里 务里出 里出现 出现频 现频率 频率一 率一直 一直不 直不低 这几年在 几年在真 年在真实 在真实业 真实业务 实业务里 业务里出 务里出现 里出现频 出现频率 现频率一 频率一直 率一直不 一直不低 尤其 其是 是带 带有 尤其是 其是带 是带有 尤其是带 其是带有 预览 图片 片抓 抓取 图片抓 片抓取 图片抓取 在线 线导 导入 在线导 线导入 在线导入 渲染 远程 程文 文件 件同 同步 远程文 程文件 文件同 件同步 远程文件 程文件同 文件同步 等能 能力 力的 的系 系统 等能力 能力的 力的系 的系统 等能力的 能力的系 力的系统 它的 的问 问题 题不 不在 在于 于某 某个 个请 请求 求函 函数 数本 本身 身危 危险 它的问 的问题 问题不 题不在 不在于 在于某 于某个 某个请 个请求 请求函 求函数 函数本 数本身 本身危 身危险 它的问题 的问题不 问题不在 题不在于 不在于某 在于某个 于某个请 某个请求 个请求函 请求函数 求函数本 函数本身 数本身危 本身危险 而是 是服 服务 务端 端替 替用 用户 户访 访问 问了 了不 不可 可信 而是服 是服务 服务端 务端替 端替用 替用户 用户访 户访问 访问了 问了不 了不可 不可信 而是服务 是服务端 服务端替 务端替用 端替用户 替用户访 用户访问 户访问了 访问了不 问了不可 了不可信 并且 且访 问环 环境 境往 往往 往比 比普 普通 通用 户浏 浏览 览器 器更 更敏 敏感 并且访 且访问 访问环 问环境 环境往 境往往 往往比 往比普 比普通 普通用 通用户 用户浏 户浏览 浏览器 览器更 器更敏 更敏感 并且访问 且访问环 访问环境 问环境往 环境往往 境往往比 往往比普 往比普通 比普通用 普通用户 通用户浏 用户浏览 户浏览器 浏览器更 览器更敏 器更敏感 这篇 篇文 文章 章整 整理 理一 一次 次较 较典 典型 型的 这篇文 篇文章 文章整 章整理 整理一 理一次 一次较 次较典 较典型 典型的 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次较 一次较典 次较典型 较典型的 排查 查思 思路 排查思 查思路 排查思路 重点 点放 放在 在代 代码 码审 审计 重点放 点放在 放在代 在代码 代码审 码审计 重点放在 点放在代 放在代码 在代码审 代码审计 复现 现验 验证 证和 和修 修复 复策 策略 略上 复现验 现验证 验证和 证和修 和修复 修复策 复策略 策略上 复现验证 现验证和 验证和修 证和修复 和修复策 修复策略 复策略上 内容 容只 只讨 讨论 论授 授权 权测 测试 试环 境中 中的 的分 分析 析方 方法 内容只 容只讨 只讨论 讨论授 论授权 授权测 权测试 测试环 试环境 环境中 境中的 中的分 的分析 分析方 析方法 内容只讨 容只讨论 只讨论授 讨论授权 论授权测 授权测试 权测试环 测试环境 试环境中 环境中的 境中的分 中的分析 的分析方 分析方法 不涉 涉及 及未 未授 权利 利用 不涉及 涉及未 及未授 未授权 授权利 权利用 不涉及未 涉及未授 及未授权 未授权利 授权利用 内网 网探 探测 测或 或攻 攻击 击扩 扩展 内网探 网探测 探测或 测或攻 或攻击 攻击扩 击扩展 内网探测 网探测或 探测或攻 测或攻击 或攻击扩 攻击扩展 场景 景描 描述 述某 某业 务提 提供 场景描 景描述 描述某 述某业 某业务 业务提 务提供 场景描述 景描述某 描述某业 述某业务 某业务提 业务提供 程图 片导 远程图 程图片 图片导 片导入 远程图片 程图片导 图片导入 功能 户提 提交 交一 一个 个图 用户提 户提交 提交一 交一个 一个图 个图片 用户提交 户提交一 提交一个 交一个图 一个图片 后端 端下 下载 载图 片并 并存 存入 入对 对象 象存 存储 后端下 端下载 下载图 载图片 图片并 片并存 并存入 存入对 入对象 对象存 象存储 后端下载 端下载图 下载图片 载图片并 图片并存 片并存入 并存入对 存入对象 入对象存 对象存储 能看 看起 起来 来很 很常 常见 功能看 能看起 看起来 起来很 来很常 很常见 功能看起 能看起来 看起来很 起来很常 来很常见 入口 口大 大概 概类 类似 入口大 口大概 大概类 概类似 入口大概 口大概类 大概类似 端逻 逻辑 辑包 包括 括三 三步 后端逻 端逻辑 逻辑包 辑包括
  9. 背景这篇整理来自一次站点被植入 WebShell 的应急复盘。目标不是讨论“如何利用”,而是把排查过程、常见痕迹、日志关联方式和后续加固点记录下来,方便以后遇到类似事件时少走弯路。 受影响环境大致是常见的 Java Web 应用,前面有 Nginx 反向代理,后端 Tomcat,业务目录存在文件上传功能。事件表现为:服务器 CPU 偶发升高,站点目录出现异常 JSP 文件,Nginx 访问日志里有少量非常规请求。 建议在授权范围内做排查和复现。本文只覆盖取证、分析和防护,不提供未授权入侵、持久化控制或规避检测的操作。问题场景初始告警来自主机侧文件完整性监控:Web 根目录下新增了几个 JSP 文件,文件名看起来像随机字符串,例如 update_2024.jsp、test1.jsp 这类容易混入日常维护文件的命名。 管理员反馈近期没有发布这些文件,也没有临时调试需求。结合访问日志查看,发现这些 JSP 文件被少量外部 IP 请求过,请求时间和文件创建时间接近。 这类事件排查重点一般有几个: 确认异常文件的写入时间、属主、权限和内容特征。从 Web 日志还原首次访问、上传入口、可疑参数。确认是否存在命令执行、横向移动、敏感文件读取等后续行为。清理落点后,定位根因并做长期加固,避免只删文件不修洞。技术分析先从文件层面看。Linux 下可以用 stat 查看文件时间,结合应用发布记录判断是否异常: stat /data/www/app/upload/update_2024.jsp ls -al --time-style=full-iso /data/www/app/upload/ find /data/www/app -type f -name "*.jsp" -mtime -7 -ls需要注意,文件的 mtime、ctime 只能作为线索,不能作为绝对证据。攻击者或者异常程序可能修改时间戳;另一方面,运维备份、解压覆盖也会改变时间。更可靠的方式是同时关联 Web 日志、系统审计日志、应用日志。 在 Nginx 场景中,可以先按异常文件名反查请求: grep -n "update_2024.jsp" /var/log/nginx/access.log* grep -n "test1.jsp" /var/log/nginx/access.log*如果日志量比较大,建议按时间窗口筛选。比如文件创建时间在 10:20 左右,可以取前后 30 分钟: awk '$4 >= "[12/Mar/2024:09:50:00" && $4 <= "[12/Mar/2024:10:50:00" {print}' /var/log/nginx/access.log不同日志格式下,awk 条件需要调整。实战里我更倾向先用 grep 粗筛 URI、状态码、IP,再导入本地做结构化分析。 日志时间线还原一次比较完整的时间线通常包含三类请求: 上传入口请求:可能是 /upload、/file/save、/editor/upload 等。落点访问请求:直接访问新写入的 JSP、PHP、ASP 文件。后续探测请求:访问系统信息、配置文件、管理接口、压缩包、备份文件等。可以先统计异常时间窗口内状态码和 URI: awk '{print $7, $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -50如果 Nginx 日志记录了请求体长度、User-Agent、Referer,会更好关联。比如上传请求一般 body_bytes_sent 或 request_length 会明显偏大;落点访问可能返回 200 且响应体较小。 常见可疑特征包括: 上传接口返回 200 或 302 后,很快访问上传目录下的脚本文件。同一 IP 在短时间内访问多个敏感路径,例如 /WEB-INF/、/actuator/、/.env。User-Agent 异常固定,或者为空,和正常浏览器请求差异明显。URL 参数包含长字符串、编码后的片段、疑似命令关键字。如果是 Tomcat,可以继续看 catalina.out、应用日志和访问日志。很多上传失败、类型校验异常、业务报错会留在应用日志里,这些信息对定位入口很有用。 异常文件内容分析拿到 WebShell 或疑似脚本后,不建议直接在生产环境打开执行。可以先复制到隔离环境,使用文本工具和静态分析方式查看。 file update_2024.jsp sha256sum update_2024.jsp strings update_2024.jsp | head -50 sed -n '1,120p' update_2024.jsp分析时关注几个点: 是否包含动态执行相关逻辑,例如反射、脚本引擎调用、进程创建、类加载等。是否读取请求参数、请求头、Cookie,并将其传入危险函数。是否对内容做 Base64、URL 编码、字符拼接等混淆。是否落地其他文件,或连接外部地址下载内容。以 Java Web 为例,风险点通常集中在: Runtime.getRuntime().exec、ProcessBuilder 这类进程调用。反射调用 ClassLoader、defineClass 等动态加载逻辑。使用 javax.script.ScriptEngine 执行脚本。读写 Web 根目录、临时目录、用户家目录下的文件。这里不建议把样本直接拿去公网沙箱提交,尤其是业务系统路径、内网地址、密钥片段可能会泄露。可以先本地脱敏,或只提取 hash 和关键行为特征。 根因定位:上传链路是重点很多 WebShell 事件的根因是文件上传校验不完整。常见问题有: 只校验前端扩展名,后端未校验。只判断 Content-Type,信任客户端传入的 MIME。允许上传目录被 Web 容器解析脚本。上传后文件名可控,路径拼接缺少规范化处理。富文本编辑器、老旧组件存在已知文件上传漏洞。代码审计时,可以搜索上传相关入口和危险写文件逻辑: grep -R "MultipartFile" -n src/main/java grep -R "transferTo" -n src/main/java grep -R "FileOutputStream" -n src/main/java grep -R "getOriginalFilename" -n src/main/java重点看文件扩展名白名单是否严格,是否使用服务端生成文件名,是否限制保存目录,是否对路径做规范化校验。 一个更稳妥的后端处理思路是: 服务端生成随机文件名,不使用用户原始文件名作为最终路径。扩展名采用白名单,例如只允许 jpg、png、pdf 等业务必需类型。结合文件头魔数做二次校验,但不要只依赖魔数。上传目录不放在 Web 可执行目录下,最好通过对象存储或静态文件服务提供访问。对上传大小、频率、用户权限做限制。关键处置步骤现场处置建议按“保全证据、隔离风险、定位根因、清理恢复、加固验证”的顺序做。不要一上来就直接删除文件,否则时间线和样本证据容易丢。 1. 保全样本和日志mkdir -p /root/incident-20240312/samples cp -a /data/www/app/upload/update_2024.jsp /root/incident-20240312/samples/ sha256sum /root/incident-20240312/samples/update_2024.jsp > /root/incident-20240312/hash.txt cp -a /var/log/nginx/access.log* /root/incident-20240312/ cp -a /var/log/nginx/error.log* /root/incident-20240312/如果主机上有审计系统或 EDR,也应导出同一时间窗口内的进程、网络连接、文件变更记录。 2. 临时隔离可疑入口如果确认上传入口存在风险,可以先临时关闭上传功能,或通过网关限制对应接口访问来源。对于已经落地的脚本文件,建议先阻断执行,而不是直接删除。 Nginx 可临时禁止上传目录解析脚本,示例: location ^~ /upload/ { location ~* \.(jsp|jspx|php|asp|aspx)$ { return 403; } }实际配置要结合业务路径调整。Java Web 下更推荐从 Tomcat 或应用部署层面确保上传目录不在可执行路径内。 3. 排查进程和网络连接WebShell 被访问后,可能触发系统命令、下载工具或反连行为。可以查看异常进程树和网络连接: ps aux --forest ss -tunap lsof -i -P -n last -a lastb -a关注 Web 服务用户启动的异常进程,例如 tomcat、www-data、nginx 用户下出现 shell、下载器、压缩工具、扫描工具等。这里只是排查方向,不建议仅凭进程名判断,需要结合启动时间、父进程、命令行和文件路径。 4. 清理落点与持久化项清理时除了 Web 根目录,还要看临时目录、计划任务、启动项、SSH 授权密钥等常见持久化位置: find /data/www/app -type f -mtime -14 -ls find /tmp /var/tmp -type f -mtime -14 -ls crontab -l ls -al /etc/cron.* ls -al ~/.ssh/authorized_keys如果存在多台节点,注意不要只清理一台。负载均衡后面的应用节点、共享存储、备份目录都要检查。 5. 修复代码和配置根因如果是上传校验问题,需要修代码,而不是靠 WAF 规则长期兜底。WAF 可以作为缓解措施,但上传目录可执行、后端校验缺失这类问题必须从应用和部署层解决。 典型配置加固包括: 上传文件存储目录与应用执行目录分离。Web 容器禁止上传目录内脚本解析。应用进程最小权限运行,不使用 root 启动。文件写权限按目录收敛,不给整个站点目录写权限。关闭不必要的管理端口和调试接口。检测规则和持续监控事件结束后,建议沉淀一些可复用的检测规则。比如对 Web 根目录新增脚本文件做监控,对上传目录出现可执行扩展名做告警。 find /data/www/app/upload -type f \( -name "*.jsp" -o -name "*.jspx" -o -name "*.php" -o -name "*.asp" -o -name "*.aspx" \) -print如果使用 auditd,可以对关键目录写入做审计: auditctl -w /data/www/app/upload -p wa -k web_upload_write aureport -f -k | grep web_upload_write生产环境中 auditd 规则要评估性能影响,尤其是高频写目录。更稳妥的方式是结合文件完整性监控、日志平台和主机安全基线统一处理。 代码审计关注点从长期治理角度,建议把以下点纳入代码审计清单: 审计点风险说明建议文件名处理使用用户原始文件名可能导致覆盖、路径穿越服务端生成文件名,路径规范化类型校验仅依赖扩展名或 Content-Type 容易失效白名单、魔数、业务侧多重校验存储位置上传目录可执行会放大风险放到非 Web 执行目录或对象存储权限控制任意用户可上传敏感类型文件按角色限制上传能力和频率异常日志上传失败和拦截缺少记录记录用户、IP、文件名、大小、原因注意点不要只看单个 WebShell 文件。一次入侵里可能有多个落点,甚至不同目录下存在备份木马。不要只依赖文件名判断。很多异常文件会伪装成业务脚本、图片缓存、临时文件。不要在生产环境直接执行样本。静态分析优先,必要时放到隔离环境。不要只删文件不修根因。上传链路、组件版本、权限模型都要检查。不要忽略时间同步。多台服务器时间不一致会影响日志关联。总结WebShell 事件的处理核心不是“找到一个文件然后删除”,而是建立完整证据链:异常文件什么时候写入、通过哪个入口写入、被谁访问、执行了什么行为、是否还有其他落点。 从防护角度看,上传目录不可执行、后端严格白名单校验、应用最小权限、关键目录文件监控,是比较基础但有效的措施。应急结束后,把排查命令、时间线、样本 hash、根因代码位置和修复记录沉淀下来,下一次响应效率会高很多。 代码审计、调用链与关键函数定位示意 背景 景这 这篇 篇整 整理 理来 来自 自一 一次 次站 站点 点被 被植 植入 背景这 景这篇 这篇整 篇整理 整理来 理来自 来自一 自一次 一次站 次站点 站点被 点被植 被植入 背景这篇 景这篇整 这篇整理 篇整理来 整理来自 理来自一 来自一次 自一次站 一次站点 次站点被 站点被植 点被植入 的应 应急 急复 复盘 的应急 应急复 急复盘 的应急复 应急复盘 目标 标不 不是 是讨 讨论 目标不 标不是 不是讨 是讨论 目标不是 标不是讨 不是讨论 如何 何利 利用 如何利 何利用 如何利用 而是 是把 把排 排查 查过 过程 而是把 是把排 把排查 排查过 查过程 而是把排 是把排查 把排查过 排查过程 常见 见痕 痕迹 常见痕 见痕迹 常见痕迹 日志 志关 关联 联方 方式 式和 和后 后续 续加 加固 固点 点记 记录 录下 下来 日志关 志关联 关联方 联方式 方式和 式和后 和后续 后续加 续加固 加固点 固点记 点记录 记录下 录下来 日志关联 志关联方 关联方式 联方式和 方式和后 式和后续 和后续加 后续加固 续加固点 加固点记 固点记录 点记录下 记录下来 方便 便以 以后 后遇 遇到 到类 类似 似事 事件 件时 时少 少走 走弯 弯路 方便以 便以后 以后遇 后遇到 遇到类 到类似 类似事 似事件 事件时 件时少 时少走 少走弯 走弯路 方便以后 便以后遇 以后遇到 后遇到类 遇到类似 到类似事 类似事件 似事件时 事件时少 件时少走 时少走弯 少走弯路 受影 影响 响环 环境 境大 大致 致是 是常 见的 受影响 影响环 响环境 环境大 境大致 大致是 致是常 是常见 常见的 受影响环 影响环境 响环境大 环境大致 境大致是 大致是常 致是常见 是常见的 应用 前面 面有 前面有 反向 向代 代理 反向代 向代理 反向代理 后端 业务 务目 目录 录存 存在 在文 文件 件上 上传 传功 功能 业务目 务目录 目录存 录存在 存在文 在文件 文件上 件上传 上传功 传功能 业务目录 务目录存 目录存在 录存在文 存在文件 在文件上 文件上传 件上传功 上传功能 件表 表现 现为 事件表 件表现 表现为 事件表现 件表现为 服务 务器 服务器 偶发 发升 升高 偶发升 发升高 偶发升高 点目 录出 出现 现异 异常 站点目 点目录 目录出 录出现 出现异 现异常 站点目录 点目录出 目录出现 录出现异 出现异常 访问 问日 志里 里有 有少 少量 量非 非常 常规 规请 请求 访问日 问日志 日志里 志里有 里有少 有少量 少量非 量非常 非常规 常规请 规请求 访问日志 问日志里 日志里有 志里有少 里有少量 有少量非 少量非常 量非常规 非常规请 常规请求 建议 议在 在授 授权 权范 范围 围内 内做 做排 查和 和复 复现 建议在 议在授 在授权 授权范 权范围 范围内 围内做 内做排 做排查 排查和 查和复 和复现 建议在授 议在授权 在授权范 授权范围 权范围内 范围内做 围内做排 内做排查 做排查和 排查和复 查和复现 本文 文只 只覆 覆盖 盖取 取证 本文只 文只覆 只覆盖 覆盖取 盖取证 本文只覆 文只覆盖 只覆盖取 覆盖取证 分析 析和 和防 防护 分析和 析和防 和防护 分析和防 析和防护 不提 提供 供未 未授 权入 入侵 不提供 提供未 供未授 未授权 授权入 权入侵 不提供未 提供未授 供未授权 未授权入 授权入侵 持久 久化 化控 控制 制或 或规 规避 避检 检测 测的 的操 操作 持久化 久化控 化控制 控制或 制或规 或规避 规避检 避检测 检测的 测的操 的操作 持久化控 久化控制 化控制或 控制或规 制或规避 或规避检 规避检测 避检测的 检测的操 测的操作 问题 题场 场景 景初 初始 始告 告警 警来 自主 主机 机侧 侧文 件完 完整 整性 性监 监控 问题场 题场景 场景初 景初始 初始告 始告警 告警来 警来自 来自主 自主机 主机侧 机侧文 侧文件 文件完 件完整 完整性 整性监 性监控 问题场景 题场景初 场景初始 景初始告 初始告警 始告警来 告警来自 警来自主 来自主机 自主机侧 主机侧文 机侧文件 侧文件完 文件完整 件完整性 完整性监 整性监控 根目 下新 新增 增了 了几 几个 根目录 目录下 录下新 下新增 新增了 增了几 了几个 根目录下 目录下新 录下新增 下新增了 新增了几 增了几个 件名 名看 看起 起来 来像 像随 随机 机字 字符 符串 文件名 件名看 名看起 看起来 起来像 来像随 像随机 随机字 机字符 字符串
  10. 背景这篇文章整理的是一次比较典型的 WebShell 排查过程。场景并不复杂:一套老旧 PHP 业务被发现存在异常外联和可疑文件落地,前端访问基本正常,业务侧最初只看到偶发 500 和目录下出现陌生 PHP 文件。类似事件在实际环境里很常见,真正有长期价值的不是某个“神奇命令”,而是把日志、文件、进程、代码审计串起来,形成可复用的排查闭环。 文中会尽量保留关键技术细节,但不会展开攻击利用或控制类操作,重点放在防守视角的取证、定位、修复和加固。 问题现象Web 目录下出现非发布流程产生的 PHP 文件,文件名接近缓存文件或图片缩略图命名。Nginx 访问日志中存在少量异常 POST 请求,User-Agent 比较固定。业务上传目录中出现可执行脚本,且部分文件修改时间集中在凌晨。应用错误日志中出现过一次上传组件报错,随后开始出现异常访问。从这些现象看,优先怀疑入口在文件上传、后台富文本、插件组件或历史遗留接口。因为文件已落地到 Web 可访问目录,排查重点要覆盖“入口点”和“执行点”两部分。 第一步:先固定现场,避免线索被覆盖很多现场排查容易一上来就删除文件或重启服务,这会破坏时间线。建议先做最小化固定: # 记录系统时间,避免后续时间线误判 date -R # 备份访问日志、错误日志和应用日志 mkdir -p /root/incident_backup/logs cp -a /var/log/nginx /root/incident_backup/logs/nginx_$(date +%F) cp -a /data/app/runtime/logs /root/incident_backup/logs/app_$(date +%F) 2>/dev/null # 记录 Web 目录近期变更文件 find /data/www -type f -mtime -7 -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' \ | sort > /root/incident_backup/recent_files.txt # 对可疑目录做只读备份 cp -a /data/www/uploads /root/incident_backup/uploads_snapshot这里有两个注意点:第一,不要只看 mtime,攻击者可能修改文件时间;第二,先复制再分析,避免编辑器、杀软或脚本扫描改变文件元数据。 第二步:从访问日志建立时间线WebShell 事件里,日志通常能回答三个问题:谁上传的、何时上传的、上传后访问了什么。先按可疑时间段过滤 POST 请求和上传接口: # 统计可疑时间段 POST 请求 awk '$4 >= "[12/Mar/2025:00:00:00" && $4 <= "[12/Mar/2025:06:00:00" && $6 ~ /POST/' \ /var/log/nginx/access.log > /tmp/post.log # 按 URL 聚合 awk '{print $7}' /tmp/post.log | sort | uniq -c | sort -nr | head -30 # 关注上传、编辑器、插件相关路径 grep -Ei 'upload|file|image|editor|ueditor|kindeditor|plugin|api' /tmp/post.log如果日志格式包含请求体长度、Referer、User-Agent、上游状态码,可以进一步聚合。真实事件中经常能看到这样的链路: 先访问后台登录页或某个公开上传接口。随后 POST 到上传接口,返回 200 或 302。几秒内访问上传目录下的 PHP 文件。之后对同一文件持续 POST,访问频率低但有规律。需要注意,日志里出现某个 IP 并不意味着它就是最终来源,可能是代理、扫描器、CDN 回源或安全设备地址。更有价值的是“同一会话行为链”和“入口 URL”。 第三步:定位可疑文件和执行痕迹对 PHP 站点,可以从高风险函数、异常编码、非常规扩展名几个方向筛选。下面这些命令适合初筛,不建议直接作为定性依据: # 查找近期新增或变更的 PHP 文件 find /data/www -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.php5' \) \ -mtime -14 -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort # 查找上传目录中的脚本文件 find /data/www/uploads -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) -ls # 初筛高风险函数和混淆特征 grep -RInE 'eval\s*\(|assert\s*\(|system\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|base64_decode\s*\(|gzinflate\s*\(' /data/www --include='*.php'单纯出现 base64_decode、call_user_func 不一定是 WebShell,很多框架和组件也会用到动态调用。更可靠的判断方式是结合上下文: 文件是否属于发布包或版本库。文件路径是否位于 uploads、cache、tmp、public/static 等不应执行脚本的目录。代码是否接收外部输入后进入动态执行、文件写入、命令执行等敏感点。文件创建时间是否与异常请求时间吻合。遇到混淆文件时,不建议在生产机器直接执行解码后的代码。可以将样本放到隔离环境,只做静态还原。还原时保留每一步输出,避免误把恶意逻辑执行起来。 第四步:回到代码,找入口而不是只删马只删除 WebShell 通常会复发。需要确认它是如何写入的。本次排查中,入口最终定位到一个历史上传接口:后端仅检查了前端传入的 MIME 类型和文件名后缀,对真实文件内容、保存路径和扩展名白名单都没有做可靠限制。 问题代码大致类似下面这种模式: // 反例:只相信客户端传入的类型和文件名 $type = $_FILES['file']['type']; $name = $_FILES['file']['name']; $tmp = $_FILES['file']['tmp_name']; if ($type === 'image/jpeg' || $type === 'image/png') { move_uploaded_file($tmp, __DIR__ . '/uploads/' . $name); }这里的问题不在于某个函数本身,而是信任边界错误:客户端传入的文件名、Content-Type 都不可信。如果上传目录又能解析 PHP,就会把一个普通上传问题扩大成远程代码执行风险。 较稳妥的修复方式包括: // 示例:白名单扩展名 + 随机文件名 + 内容识别 + 非 Web 根目录存储 $allowExt = ['jpg', 'jpeg', 'png', 'gif']; $originName = $_FILES['file']['name'] ?? ''; $tmp = $_FILES['file']['tmp_name'] ?? ''; $ext = strtolower(pathinfo($originName, PATHINFO_EXTENSION)); if (!in_array($ext, $allowExt, true)) { http_response_code(400); exit('invalid extension'); } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($tmp); $allowMime = ['image/jpeg', 'image/png', 'image/gif']; if (!in_array($mime, $allowMime, true)) { http_response_code(400); exit('invalid mime'); } $newName = bin2hex(random_bytes(16)) . '.' . $ext; $saveDir = '/data/upload_store/' . date('Ymd'); if (!is_dir($saveDir)) { mkdir($saveDir, 0750, true); } move_uploaded_file($tmp, $saveDir . '/' . $newName);如果业务必须让文件可通过 URL 访问,建议通过下载接口或对象存储访问,不要直接把上传目录放进可执行的 Web 根目录。 第五步:限制上传目录执行能力即使代码层修复了,也建议在 Web 服务器层面加一道保险。Nginx 场景下,可以禁止上传目录解析脚本: location ^~ /uploads/ { alias /data/www/uploads/; autoindex off; location ~* \.(php|phtml|php5|phar)$ { return 403; } }如果 PHP-FPM 配置中存在宽泛匹配,也要一起检查,避免某些路径绕过 location 限制。常见需要关注的配置包括: 是否存在 location ~ \.php 过于宽泛的匹配。是否开启不必要的 PATH_INFO 解析。上传目录是否被 alias 到 Web 可执行路径。PHP-FPM 用户是否拥有 Web 根目录写权限。Apache 环境可以用目录级配置禁用脚本执行,但更推荐从虚拟主机配置层统一限制,避免依赖可被覆盖的 .htaccess。 第六步:检查持久化和横向痕迹WebShell 落地后,不排除攻击者尝试写计划任务、替换文件、创建隐藏账号或读取配置。排查时建议覆盖以下位置: # 计划任务 crontab -l ls -la /var/spool/cron /etc/cron.* /etc/crontab 2>/dev/null # 最近变更的系统脚本和临时目录 find /tmp /var/tmp /dev/shm -type f -mtime -14 -ls 2>/dev/null # Web 目录异常权限 find /data/www -type f -perm -111 -ls find /data/www -type d -perm -002 -ls # 进程和网络连接基线 ps auxww ss -antup还要重点检查应用配置文件是否泄露过数据库账号、对象存储密钥、第三方接口密钥。如果日志显示可疑文件访问过配置目录,建议轮换相关凭据,而不是只改后台密码。 代码审计关注点围绕这类事件,后续代码审计可以按数据流梳理,不要只搜危险函数。建议关注以下几类入口: 审计点典型风险检查建议文件上传脚本落地、任意文件写入扩展名白名单、内容识别、随机文件名、非 Web 根存储文件管理目录穿越、删除/覆盖敏感文件路径规范化、限制根目录、禁止用户控制完整路径模板渲染模板注入、写入可执行模板区分模板目录和用户内容目录,限制后台编辑能力插件机制未授权安装、历史组件漏洞关闭不必要插件,校验插件来源和版本后台接口越权调用、弱鉴权统一鉴权中间件,避免只依赖前端隐藏入口对老项目来说,优先处理“可写目录可执行”“上传接口无白名单”“后台弱口令或无 MFA”“历史组件未升级”这几类问题,收益通常比大范围重构更明显。 日志与监控建议事件后建议补齐最基本的检测能力,不需要一开始就上很复杂的平台。几个低成本但有效的点: 记录上传接口的用户 ID、源 IP、文件原名、保存名、大小、MIME、处理结果。对 Web 目录新增 PHP 文件做定时检测,特别是 uploads、cache、static、tmp。对异常 POST 到图片、静态目录、上传目录的请求做告警。对配置文件、入口文件、核心框架目录做完整性校验。保留足够长的访问日志和应用日志,至少覆盖一个业务周期。# 简单完整性基线示例 find /data/www/app /data/www/public -type f \ -not -path '*/runtime/*' -not -path '*/uploads/*' \ -exec sha256sum {} \; | sort > /root/www_baseline.sha256 # 后续比对 find /data/www/app /data/www/public -type f \ -not -path '*/runtime/*' -not -path '*/uploads/*' \ -exec sha256sum {} \; | sort > /tmp/www_current.sha256 diff -u /root/www_baseline.sha256 /tmp/www_current.sha256几个容易忽略的注意点不要只清理一个样本文件,要根据访问日志确认是否存在多个落地点。不要只改后台密码,如果数据库、对象存储、短信网关密钥泄露过,应同步轮换。不要在生产环境直接执行可疑样本,静态分析优先,必要时放隔离环境。不要过度依赖文件后缀判断,Web 服务器解析规则和历史兼容配置可能带来额外风险。修复后要复测入口是否真正关闭,并观察一段时间是否还有旧路径访问。总结WebShell 事件的处理重点是闭环:先固定现场,再用日志建立时间线,定位落地文件和访问行为,回到代码找写入入口,最后做配置加固、凭据轮换和监控补齐。只删文件通常解决不了根因,甚至会掩盖线索。 从经验看,上传目录可执行、老组件未维护、后台接口鉴权松散,是这类事件里最常见的组合风险。把这些基础问题处理扎实,比临时堆很多规则更可靠。真正有价值的复盘,不是证明“被打过”,而是让同类问题下次更早被发现、更难被利用、修复成本更低。 网络请求、日志与边界流量分析示意 背景 景这 这篇 篇文 文章 章整 整理 理的 的是 是一 一次 次比 比较 较典 典型 型的 背景这 景这篇 这篇文 篇文章 文章整 章整理 整理的 理的是 的是一 是一次 一次比 次比较 比较典 较典型 典型的 背景这篇 景这篇文 这篇文章 篇文章整 文章整理 章整理的 整理的是 理的是一 的是一次 是一次比 一次比较 次比较典 比较典型 较典型的 排查 查过 过程 排查过 查过程 排查过程 场景 景并 并不 不复 复杂 场景并 景并不 并不复 不复杂 场景并不 景并不复 并不复杂 一套 套老 老旧 一套老 套老旧 一套老旧 业务 务被 被发 发现 现存 存在 在异 异常 常外 外联 联和 和可 可疑 疑文 文件 件落 落地 业务被 务被发 被发现 发现存 现存在 存在异 在异常 异常外 常外联 外联和 联和可 和可疑 可疑文 疑文件 文件落 件落地 业务被发 务被发现 被发现存 发现存在 现存在异 存在异常 在异常外 异常外联 常外联和 外联和可 联和可疑 和可疑文 可疑文件 疑文件落 文件落地 前端 端访 访问 问基 基本 本正 正常 前端访 端访问 访问基 问基本 基本正 本正常 前端访问 端访问基 访问基本 问基本正 基本正常 务侧 侧最 最初 初只 只看 看到 到偶 偶发 业务侧 务侧最 侧最初 最初只 初只看 只看到 看到偶 到偶发 业务侧最 务侧最初 侧最初只 最初只看 初只看到 只看到偶 看到偶发 和目 目录 录下 下出 出现 现陌 陌生 和目录 目录下 录下出 下出现 出现陌 现陌生 和目录下 目录下出 录下出现 下出现陌 出现陌生 类似 似事 事件 件在 在实 实际 际环 环境 境里 里很 很常 常见 类似事 似事件 事件在 件在实 在实际 实际环 际环境 环境里 境里很 里很常 很常见 类似事件 似事件在 事件在实 件在实际 在实际环 实际环境 际环境里 环境里很 境里很常 里很常见 真正 正有 有长 长期 期价 价值 值的 的不 不是 是某 某个 真正有 正有长 有长期 长期价 期价值 价值的 值的不 的不是 不是某 是某个 真正有长 正有长期 有长期价 长期价值 期价值的 价值的不 值的不是 的不是某 不是某个 神奇 奇命 命令 神奇命 奇命令 神奇命令 而是 是把 把日 日志 而是把 是把日 把日志 而是把日 是把日志 进程 代码 码审 审计 计串 串起 起来 代码审 码审计 审计串 计串起 串起来 代码审计 码审计串 审计串起 计串起来 形成 成可 可复 复用 用的 的排 查闭 闭环 形成可 成可复 可复用 复用的 用的排 的排查 排查闭 查闭环 形成可复 成可复用 可复用的 复用的排 用的排查 的排查闭 排查闭环 文中 中会 会尽 尽量 量保 保留 留关 关键 键技 技术 术细 细节 文中会 中会尽 会尽量 尽量保 量保留 保留关 留关键 关键技 键技术 技术细 术细节 文中会尽 中会尽量 会尽量保 尽量保留 量保留关 保留关键 留关键技 关键技术 键技术细 技术细节 但不 不会 会展 展开 开攻 攻击 击利 利用 用或 或控 控制 制类 类操 操作 但不会 不会展 会展开 展开攻 开攻击 攻击利 击利用 利用或 用或控 或控制 控制类 制类操 类操作 但不会展 不会展开 会展开攻 展开攻击 开攻击利 攻击利用 击利用或 利用或控 用或控制 或控制类 控制类操 制类操作 重点 点放 放在 在防 防守 守视 视角 角的 的取 取证 重点放 点放在 放在防 在防守 防守视 守视角 视角的 角的取 的取证 重点放在 点放在防 放在防守 在防守视 防守视角 守视角的 视角的取 角的取证 定位 修复 复和 和加 加固 修复和 复和加 和加固 修复和加 复和加固 问题 题现 现象 问题现 题现象 问题现象 现非 非发 发布 布流 流程 程产 产生 生的 出现非 现非发 非发布 发布流 布流程 流程产 程产生 产生的 下出现非 出现非发 现非发布 非发布流 发布流程 布流程产 流程产生 程产生的 件名 名接 接近 近缓 缓存 存文 件或 或图 图片 片缩 缩略 略图 图命 命名 文件名 件名接 名接近 接近缓 近缓存 缓存文 存文件 文件或 件或图 或图片 图片缩 片缩略 缩略图 略图命 图命名 文件名接 件名接近 名接近缓 接近缓存 近缓存文 缓存文件 存文件或 文件或图 件或图片 或图片缩 图片缩略 片缩略图 缩略图命 略图命名 问日 志中 中存 在少 少量 量异 访问日 问日志 日志中 志中存 中存在 存在少 在少量 少量异 量异常 访问日志 问日志中 日志中存 志中存在 中存在少 存在少量 在少量异 少量异常 请求 较固
  11. 背景最近在一个授权测试项目里复现了一处典型的 Web 业务漏洞。漏洞本身并不复杂,但它比较适合作为代码审计和漏洞复现的案例:入口看起来是普通查询接口,后端使用了 ORM,但在局部代码里为了拼接动态条件退回了字符串拼接,最终造成可控参数进入 SQL 片段。 这类问题在真实项目里很常见:主框架是安全的,基础库也比较规范,但业务开发为了快速实现筛选、排序、导出等功能,在某些边角逻辑里绕过了统一封装。本文整理复现思路、定位方式和修复建议,重点放在授权环境下的分析流程,不涉及未授权攻击或破坏性操作。 问题场景目标系统提供了一个列表查询接口,用于后台运营人员查看订单记录。接口支持按状态、时间、关键字查询,同时支持前端传入排序字段和排序方向。 GET /api/order/list?status=paid&keyword=test&sortField=create_time&sortOrder=desc从功能设计上看,查询参数大致分为两类: 值类型参数:如 status、keyword、startTime、endTime,通常进入 where 条件。结构类型参数:如 sortField、sortOrder,通常进入 order by 片段。很多团队会重视 where 条件里的参数绑定,但容易忽视 order by、group by、limit 等结构片段。这些位置通常不能直接使用普通占位符绑定字段名,如果没有白名单校验,就会形成注入风险。 技术分析审计时先从路由入口定位控制器方法。简化后的代码如下: @GetMapping("/list") public PageResult<OrderVO> list(OrderQuery query) { return orderService.queryList(query); }继续跟进 service 层,可以看到普通筛选条件使用了参数化封装: public PageResult<OrderVO> queryList(OrderQuery query) { QueryWrapper<Order> wrapper = new QueryWrapper<>(); if (StringUtils.hasText(query.getStatus())) { wrapper.eq("status", query.getStatus()); } if (StringUtils.hasText(query.getKeyword())) { wrapper.like("order_no", query.getKeyword()); } if (StringUtils.hasText(query.getSortField())) { wrapper.last("order by " + query.getSortField() + " " + query.getSortOrder()); } return orderMapper.selectPage(query.toPage(), wrapper); }问题点在 wrapper.last。类似方法通常会把传入内容直接拼到 SQL 末尾,不再做参数化处理。sortField 和 sortOrder 来自 HTTP 请求,如果没有白名单限制,就相当于把用户输入接入了 SQL 结构。 从风险角度看,这类漏洞不一定都能直接读取敏感数据,取决于数据库类型、驱动配置、SQL 执行方式和权限。但它至少会造成查询逻辑被篡改、异常回显、时间延迟、稳定性影响等问题。对于后台系统,还可能被组合利用来扩大影响面。 复现思路授权环境下复现时,建议遵循“低影响、可证明、可回滚”的原则。这里不使用破坏性语句,也不尝试越权读取敏感数据,只验证参数是否进入 SQL 结构以及后端是否按输入执行。 第一步,确认正常请求: GET /api/order/list?status=paid&sortField=create_time&sortOrder=desc观察响应状态、排序结果和服务端日志,记录一组基线。 第二步,使用无害表达式验证排序字段是否可控。例如在测试库中选择一个不会改变数据的表达式作为排序项,观察 SQL 日志或结果变化: GET /api/order/list?status=paid&sortField=id&sortOrder=ascGET /api/order/list?status=paid&sortField=id&sortOrder=desc如果 asc 和 desc 能稳定改变顺序,说明排序方向参数生效。随后检查 sortField 是否仅允许预期字段。如果传入不存在字段会导致数据库错误,并且错误栈能在日志中看到拼接后的 SQL,基本可以确认缺少字段白名单。 GET /api/order/list?status=paid&sortField=not_exists_column&sortOrder=desc第三步,结合服务端 SQL 日志确认拼接位置。测试环境可以临时开启 ORM SQL 输出,不建议在生产环境开启详细 SQL 日志。 logging: level: com.example.order.mapper: debug如果日志中出现类似内容: select * from t_order where status = ? order by not_exists_column desc即可证明 sortField 被直接拼入 order by 子句。这里已经足够支撑漏洞结论,不需要进一步构造高风险载荷。 关键定位方法这类问题在代码审计中可以通过关键字快速收敛。Java 项目里常见关注点包括: MyBatis XML 中的 ${},尤其是 order by、group by、tableName、columnName。QueryWrapper.last、apply、inSql、exists 等会拼接 SQL 片段的方法。手写 StringBuilder 拼 SQL,特别是拼接排序字段、导出字段、动态表名。Controller 直接接收 Map、JSONObject,然后透传到 DAO 层。可以先用 ripgrep 做一次全局扫描: rg "\$\{|\.last\(|\.apply\(|StringBuilder|order by|group by|limit" ./src/main如果项目使用 MyBatis XML,重点看这些模式: ORDER BY ${sortField} ${sortOrder} GROUP BY ${groupField} LIMIT ${offset}, ${pageSize}其中 ${} 是文本替换,#{} 才是预编译参数绑定。字段名、表名这类结构无法简单用 #{} 替代,正确做法通常是白名单映射。 修复建议修复核心是:用户只能选择业务允许的排序字段和排序方向,不能直接控制 SQL 片段。 一个比较稳妥的做法是建立字段映射表: private static final Map<String, String> SORT_FIELD_MAP = Map.of( "createTime", "create_time", "payTime", "pay_time", "amount", "amount", "id", "id" ); public PageResult<OrderVO> queryList(OrderQuery query) { QueryWrapper<Order> wrapper = new QueryWrapper<>(); if (StringUtils.hasText(query.getStatus())) { wrapper.eq("status", query.getStatus()); } String sortColumn = SORT_FIELD_MAP.getOrDefault(query.getSortField(), "create_time"); boolean asc = "asc".equalsIgnoreCase(query.getSortOrder()); wrapper.orderBy(true, asc, sortColumn); return orderMapper.selectPage(query.toPage(), wrapper); }注意这里不是简单判断 sortField 是否匹配正则。正则只能限制字符形态,不能保证字段属于业务允许范围。白名单映射还能避免前端字段名和数据库字段名强耦合。 如果是 MyBatis XML,可以使用 choose 做白名单分支: ORDER BY <choose> <when test="sortField == 'createTime'">create_time</when> <when test="sortField == 'payTime'">pay_time</when> <when test="sortField == 'amount'">amount</when> <otherwise>create_time</otherwise> </choose> <choose> <when test="sortOrder == 'asc'">ASC</when> <otherwise>DESC</otherwise> </choose>分页参数也要限制范围,避免极大 pageSize 造成慢查询: int pageSize = Math.min(Math.max(query.getPageSize(), 1), 100);验证修复修复后建议补充三类测试: 正常字段:createTime、amount 等应能正常排序。非法字段:不存在字段、带特殊字符的字段应回退到默认排序或返回参数错误。非法方向:除 asc 外统一按 desc 或返回参数错误,行为要稳定。可以加入单元测试或接口测试,避免后续重构再次引入同类问题: @Test void shouldFallbackWhenSortFieldInvalid() { OrderQuery query = new OrderQuery(); query.setSortField("not_exists_column"); query.setSortOrder("desc"); PageResult<OrderVO> result = orderService.queryList(query); assertNotNull(result); }如果团队有代码扫描流程,也可以把危险 API 纳入规则。例如发现 wrapper.last 接收外部参数时给出告警,MyBatis XML 中出现 ${sortField} 时要求人工确认。 注意点不要把“使用 ORM”等同于“没有 SQL 注入”。ORM 只能降低风险,不能替代输入边界控制。order by、group by、动态表名、动态列名是审计重点,因为这些位置经常被文本拼接。复现阶段尽量使用低影响方式证明问题,不要在非隔离环境执行破坏性语句。错误信息不要直接返回前端,生产环境应统一异常响应,详细 SQL 只保留在受控日志中。权限也要最小化。即使存在注入点,数据库账号也不应拥有超出业务需要的权限。总结这次案例的根因不是框架缺陷,而是业务代码在处理动态排序时缺少白名单。类似问题往往隐藏在“看起来只是列表查询”的接口里,危害容易被低估。 实际审计时,可以围绕结构型 SQL 片段建立检查清单:排序、分组、分页、动态表名、导出字段。复现时保持低影响,修复时使用白名单映射,并补充测试和扫描规则。这样不只是修掉一个漏洞,也能降低同类问题在后续迭代中反复出现的概率。 代码审计、调用链与关键函数定位示意 背景 景最 最近 近在 在一 一个 个授 授权 权测 测试 试项 项目 目里 里复 复现 现了 了一 一处 处典 典型 型的 背景最 景最近 最近在 近在一 在一个 一个授 个授权 授权测 权测试 测试项 试项目 项目里 目里复 里复现 复现了 现了一 了一处 一处典 处典型 典型的 背景最近 景最近在 最近在一 近在一个 在一个授 一个授权 个授权测 授权测试 权测试项 测试项目 试项目里 项目里复 目里复现 里复现了 复现了一 现了一处 了一处典 一处典型 处典型的 业务 务漏 漏洞 业务漏 务漏洞 业务漏洞 洞本 本身 身并 并不 不复 复杂 漏洞本 洞本身 本身并 身并不 并不复 不复杂 漏洞本身 洞本身并 本身并不 身并不复 并不复杂 但它 它比 比较 较适 适合 合作 作为 为代 代码 码审 审计 计和 和漏 洞复 现的 的案 案例 但它比 它比较 比较适 较适合 适合作 合作为 作为代 为代码 代码审 码审计 审计和 计和漏 和漏洞 漏洞复 洞复现 复现的 现的案 的案例 但它比较 它比较适 比较适合 较适合作 适合作为 合作为代 作为代码 为代码审 代码审计 码审计和 审计和漏 计和漏洞 和漏洞复 漏洞复现 洞复现的 复现的案 现的案例 入口 口看 看起 起来 来是 是普 普通 通查 查询 询接 接口 入口看 口看起 看起来 起来是 来是普 是普通 普通查 通查询 查询接 询接口 入口看起 口看起来 看起来是 起来是普 来是普通 是普通查 普通查询 通查询接 查询接口 后端 端使 使用 用了 后端使 端使用 使用了 后端使用 端使用了 但在 在局 局部 部代 码里 里为 为了 了拼 拼接 接动 动态 态条 条件 件退 退回 回了 了字 字符 符串 串拼 但在局 在局部 局部代 部代码 代码里 码里为 里为了 为了拼 了拼接 拼接动 接动态 动态条 态条件 条件退 件退回 退回了 回了字 了字符 字符串 符串拼 串拼接 但在局部 在局部代 局部代码 部代码里 代码里为 码里为了 里为了拼 为了拼接 了拼接动 拼接动态 接动态条 动态条件 态条件退 条件退回 件退回了 退回了字 回了字符 了字符串 字符串拼 符串拼接 最终 终造 造成 成可 可控 控参 参数 数进 进入 最终造 终造成 造成可 成可控 可控参 控参数 参数进 数进入 最终造成 终造成可 造成可控 成可控参 可控参数 控参数进 参数进入 片段 这类 类问 问题 题在 在真 真实 实项 里很 很常 常见 这类问 类问题 问题在 题在真 在真实 真实项 实项目 目里很 里很常 很常见 这类问题 类问题在 问题在真 题在真实 在真实项 真实项目 实项目里 项目里很 目里很常 里很常见 主框 框架 架是 是安 安全 全的 主框架 框架是 架是安 是安全 安全的 主框架是 框架是安 架是安全 是安全的 基础 础库 库也 也比 较规 规范 基础库 础库也 库也比 也比较 比较规 较规范 基础库也 础库也比 库也比较 也比较规 比较规范 但业 务开 开发 发为 了快 快速 速实 实现 现筛 筛选 但业务 业务开 务开发 开发为 发为了 为了快 了快速 快速实 速实现 实现筛 现筛选 但业务开 业务开发 务开发为 开发为了 发为了快 为了快速 了快速实 快速实现 速实现筛 实现筛选 排序 导出 出等 等功 功能 导出等 出等功 等功能 导出等功 出等功能 在某 某些 些边 边角 角逻 逻辑 辑里 里绕 绕过 过了 了统 统一 一封 封装 在某些 某些边 些边角 边角逻 角逻辑 逻辑里 辑里绕 里绕过 绕过了 过了统 了统一 统一封 一封装 在某些边 某些边角 些边角逻 边角逻辑 角逻辑里 逻辑里绕 辑里绕过 里绕过了 绕过了统 过了统一 了统一封 统一封装 本文 文整 整理 理复 现思 思路 本文整 文整理 整理复 理复现 复现思 现思路 本文整理 文整理复 整理复现 理复现思 复现思路 定位 位方 方式 式和 和修 修复 复建 建议 定位方 位方式 方式和 式和修 和修复 修复建 复建议 定位方式 位方式和 方式和修 式和修复 和修复建 修复建议 重点 点放 放在 在授 权环 环境 境下 下的 的分 分析 析流 流程 重点放 点放在 放在授 在授权 授权环 权环境 环境下 境下的 下的分 的分析 分析流 析流程 重点放在 点放在授 放在授权 在授权环 授权环境 权环境下 环境下的 境下的分 下的分析 的分析流 分析流程 不涉 涉及 及未 未授 权攻 攻击 击或 或破 破坏 坏性 性操 操作 不涉及 涉及未 及未授 未授权 授权攻 权攻击 攻击或 击或破 或破坏 破坏性 坏性操 性操作 不涉及未
  12. SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可控 URL”这么简单。真正有价值的分析,需要回答三个问题:请求是否真的由服务端发起、请求能否到达不应暴露的网络区域、攻击者能否读取或影响响应结果。本文以代码审计和安全验证为主线,整理一套适用于 Web 服务、Webhook、图片抓取和文档解析功能的 SSRF 分析方法。 一、典型场景与风险边界常见的 SSRF 入口包括远程图片下载、URL 预览、Webhook 回调、PDF 或网页转码、第三方接口连通性检测等。开发者通常会接收一个 URL,然后由后端使用 HTTP 客户端访问该地址,最后将状态码、响应体或部分响应信息返回给用户。 风险并不只来自访问内网地址。即使目标服务无法直接读取响应,仍可能通过请求时间、状态码、错误信息或日志造成信息泄露。因此,审计时应分别评估网络可达性、响应可见性和请求副作用。 风险维度需要关注的问题网络范围是否可以访问本机、容器网段、办公网或管理网段协议能力是否仅允许 HTTP/HTTPS,是否错误地支持其他协议响应处理是否把完整响应、错误信息或响应头返回给用户请求副作用是否会触发内部服务的写操作、任务执行或状态变更解析一致性域名解析、重定向和连接时使用的地址是否经过同一套校验二、代码审计:从输入源追踪到网络出口审计时不要只搜索“url”或“request”关键字,更有效的方法是先找外部输入,再沿数据流追踪到网络请求函数。常见入口包括 JSON 字段、表单参数、HTTP Header、数据库中的回调地址以及消息队列中的任务参数。 以下是一个具有代表性的危险写法: def fetch_preview(user_url): response = requests.get(user_url, timeout=10) return response.text这段代码的问题不只在于缺少域名限制,还包括没有限制协议、没有限制响应大小、默认跟随重定向,并且直接把完整响应返回给调用方。审计报告中应明确指出每个问题对应的影响,而不是笼统地写成“存在 SSRF”。 进一步分析时,可以建立如下调用链: HTTP 参数 url -> URL 规范化 -> 域名解析 -> IP 访问控制 -> HTTP 客户端连接 -> 重定向处理 -> 响应读取与返回如果校验发生在错误的位置,例如只检查初始 URL 字符串,却没有检查重定向后的目标,或者校验时解析一次地址、连接时又重新解析一次地址,就可能形成校验与实际请求不一致的问题。 三、安全验证:使用本地实验环境完成闭环验证 SSRF 时,建议使用本地构造的两个服务:一个作为业务服务,接收用户提供的 URL;另一个作为模拟内部服务,仅监听本机或测试网段。这样既能确认请求是否由服务端发起,也不会触碰真实生产系统。 模拟内部服务可以只返回固定内容: from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_GET(self): body = b"internal-test-service" self.send_response(200) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()验证时重点记录以下证据: 业务服务的访问日志中是否出现测试请求。模拟内部服务是否收到连接,以及请求路径和请求头。业务接口返回的是完整响应、状态码、错误信息还是超时。请求是否经过代理、DNS 服务或其他中间组件。重定向、超时和大响应场景下,服务是否仍然稳定。不要把“本机地址可访问”直接等同于高危。风险等级还取决于服务进程权限、网络分段、响应可见性以及目标接口是否具备写操作。报告应将可验证事实与推测性影响分开描述。 四、URL 校验中最容易遗漏的细节SSRF 防护的难点在于 URL 是一个结构化对象,不能只用字符串前缀判断。例如,检查 URL 是否以某个域名开头,无法可靠区分真正的主机名和只是出现在用户名、路径或查询参数中的字符串。 建议至少执行以下步骤: 只允许业务确实需要的协议,通常为 HTTP 和 HTTPS。解析 URL 后单独获取主机名、端口和路径,不使用字符串包含判断。拒绝用户名、密码和不必要的非标准端口。对主机名进行规范化,处理大小写、末尾点和国际化域名等情况。解析所有 A 和 AAAA 记录,并对每个结果执行地址范围检查。明确拒绝回环地址、链路本地地址、私有地址、组播地址、未指定地址及企业内部保留网段。禁用或严格控制自动重定向,并对每一次重定向重新执行完整校验。限制响应体大小、连接时间、读取时间和总请求时间。IPv4 和 IPv6 必须同时考虑。很多实现只判断常见的 IPv4 私网段,却遗漏 IPv6 回环地址或链路本地地址。还要注意 DNS 解析结果可能有多个地址,不能只检查第一个结果。 五、一个更稳妥的防护实现思路下面示例展示防护逻辑的结构,重点是分层校验,不代表可以直接替代经过充分测试的生产级 HTTP 客户端。 from urllib.parse import urlparse import ipaddress import socket ALLOWED_SCHEMES = {"http", "https"} class UnsafeTarget(Exception): pass def validate_target(raw_url): parsed = urlparse(raw_url) if parsed.scheme.lower() not in ALLOWED_SCHEMES: raise UnsafeTarget("unsupported scheme") if not parsed.hostname or parsed.username or parsed.password: raise UnsafeTarget("invalid host information") if parsed.port not in (None, 80, 443): raise UnsafeTarget("unsupported port") host = parsed.hostname.rstrip(".").lower() addresses = socket.getaddrinfo(host, parsed.port or 443, type=socket.SOCK_STREAM) if not addresses: raise UnsafeTarget("no address") for item in addresses: ip = ipaddress.ip_address(item[4][0]) if (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified): raise UnsafeTarget("blocked address") return parsed实际实现还应避免解析与连接之间的地址漂移问题。较稳妥的方式是由受控的网络层或代理统一执行出口策略,并在连接阶段绑定经过校验的地址;如果使用域名直接交给底层客户端,应确认底层解析、连接和重定向行为符合预期。 六、重定向、DNS 与代理带来的差异重定向是审计中经常被忽略的路径。初始地址可能属于允许访问的站点,但响应中的 Location 可能指向另一个网络区域。因此,禁用自动重定向通常是更容易审计的选择;如果业务必须支持重定向,则每一跳都应重新解析、校验并限制最大跳数。 DNS 相关问题也不能只靠一次解析解决。域名可能返回多个地址,解析结果可能变化,代理还可能在另一侧重新解析域名。防护策略应明确请求是由应用直连还是由出站代理代发,并在网络层设置默认拒绝的出口规则,避免仅依赖应用代码。 代码校验解决“应用认为目标是否可信”,网络出口策略解决“即使代码出现遗漏,服务实际上还能访问什么”。两者应同时存在。 七、响应处理和资源消耗即使目标地址校验正确,远程抓取功能仍可能被滥用于资源消耗。常见问题包括无限读取响应、压缩包或转码任务过大、连接池耗尽,以及对慢速响应长期等待。 设置连接超时、读取超时和总超时,不要只设置一个超时参数。使用流式读取,并限制最大响应字节数。限制并发任务数量,对同一用户或同一目标做速率控制。禁止自动解压不可信内容,或对解压后的文件数量和总大小设上限。不要把远端响应头原样转发给浏览器,避免引入缓存、跳转和内容类型问题。日志中记录目标域名、解析结果、最终状态、耗时和拒绝原因,但避免记录敏感响应正文。八、审计报告应该怎样落地一份可执行的 SSRF 报告至少应包含:受影响接口、输入参数、调用链、验证环境、请求是否成功到达测试服务、响应是否可见、可访问的网络范围、影响分析和修复建议。 修复建议不要只写“增加 URL 白名单”。应根据业务模式给出具体方案:固定第三方服务时使用精确域名白名单;只需要回调时采用预注册的目标标识而不是直接接受完整 URL;必须抓取任意公网资源时,使用独立出站代理、默认拒绝的网络策略和严格的资源限制。 九、总结SSRF 分析的核心不是寻找某个神奇的绕过写法,而是建立从输入、解析、DNS、连接、重定向到响应处理的完整链路。审计时应同时关注协议、地址范围、解析一致性、网络出口和资源消耗;验证时使用本地模拟服务保留可复现证据;修复时采用应用层校验与网络层隔离的纵深防御。 对于长期维护的系统,建议将 SSRF 防护纳入代码评审清单和回归测试,并对 HTTP 客户端、代理配置及依赖库升级进行专项复核。这样才能避免一次修复后,因新增抓取功能或更换网络组件再次引入同类问题。 服务器、云资产与权限边界梳理示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不是 并不是 发现 现一 一个 个可 可控 发现一 现一个 一个可 个可控 发现一个 现一个可 一个可控 这么 么简 简单 这么简 么简单 这么简单 真正 正有 有价 价值 值的 的分 分析 真正有 正有价 有价值 价值的 值的分 的分析 真正有价 正有价值 有价值的 价值的分 值的分析 需要 要回 回答 答三 三个 个问 问题 需要回 要回答 回答三 答三个 三个问 个问题 需要回答 要回答三 回答三个 答三个问 三个问题 求是 是否 否真 真的 的由 由服 端发 发起 请求是 求是否 是否真 否真的 真的由 的由服 由服务 务端发 端发起 请求是否 求是否真 是否真的 否真的由 真的由服 的由服务 由服务端 服务端发 务端发起 求能 能否 否到 到达 达不 不应 应暴 暴露 露的 的网 网络 络区 区域 请求能 求能否 能否到 否到达 到达不 达不应 不应暴 应暴露 暴露的 露的网 的网络 网络区 络区域 请求能否 求能否到 能否到达 否到达不 到达不应 达不应暴 不应暴露 应暴露的 暴露的网 露的网络 的网络区 网络区域 攻击 击者 者能 否读 读取 取或 或影 影响 响响 响应 应结 结果 攻击者 击者能 者能否 能否读 否读取 读取或 取或影 或影响 影响响 响响应 响应结 应结果 攻击者能 击者能否 者能否读 能否读取 否读取或 读取或影 取或影响 或影响响 影响响应 响响应结 响应结果 本文 文以 以代 代码 码审 审计 计和 和安 安全 全验 验证 证为 为主 主线 本文以 文以代 以代码 代码审 码审计 审计和 计和安 和安全 安全验 全验证 验证为 证为主 为主线 本文以代 文以代码 以代码审 代码审计 码审计和 审计和安 计和安全 和安全验 安全验证 全验证为 验证为主 证为主线 整理 理一 一套 套适 适用 用于 整理一 理一套 一套适 套适用 适用于 整理一套 理一套适 一套适用 套适用于 图片 片抓 抓取 取和 和文 文档 档解 解析 析功 功能 能的 图片抓 片抓取 抓取和 取和文 和文档 文档解 档解析 解析功 析功能 功能的 图片抓取 片抓取和 抓取和文 取和文档 和文档解 文档解析 档解析功 解析功能 析功能的 析方 方法 分析方 析方法 分析方法 典型 型场 场景 景与 与风 风险 险边 边界 界常 常见 见的 典型场 型场景 场景与 景与风 与风险 风险边 险边界 边界常 界常见 常见的 典型场景 型场景与 场景与风 景与风险 与风险边 风险边界 险边界常 边界常见 界常见的 入口 口包 包括 括远 远程 程图 片下 下载 入口包 口包括 包括远 括远程 远程图 程图片 图片下 片下载 入口包括 口包括远 包括远程 括远程图 远程图片 程图片下 图片下载 预览 回调 或网 网页 页转 转码 或网页 网页转 页转码 或网页转 网页转码 第三 三方 方接 接口 口连 连通 通性 性检 检测 测等 第三方 三方接 方接口 接口连 口连通 连通性 通性检 性检测 检测等 第三方接 三方接口 方接口连 接口连通 口连通性 连通性检 通性检测 性检测等 开发 发者 者通 通常 常会 会接 接收 收一 开发者 发者通 者通常 通常会 常会接 会接收 接收一 收一个 开发者通 发者通常 者通常会 通常会接 常会接收 会接收一 接收一个 然后 后由 由后 后端 端使 使用 然后由 后由后 由后端 后端使 端使用 然后由后 后由后端 由后端使 后端使用 客户 户端 端访 访问 问该 该地 地址 客户端 户端访 端访问 访问该 问该地 该地址 客户端访 户端访问 端访问该 访问该地 问该地址 最后 后将 将状 状态 态码 最后将 后将状 将状态 状态码 最后将状 后将状态 将状态码 应体 体或 或部 部分 分响 应信 信息 息返 返回 回给 给用 用户 响应体 应体或 体或部 或部分 部分响 分响应 响应信 应信息 信息返 息返回 返回给 回给用 给用户 响应体或 应体或部 体或部分 或部分响 部分响应 分响应信 响应信息 应信息返 信息返回 息返回给 返回给用 回给用户 险并 不只 只来 来自 自访 问内 内网 网地 风险并 险并不 并不只 不只来 只来自 来自访 自访问 访问内 问内网 内网地 网地址 风险并不 险并不只 并不只来 不只来自 只来自访 来自访问 自访问内 访问内网 问内网地 内网地址 即使 使目 目标 标服 务无 无法 法直 直接 接读 取响 即使目 使目标 目标服 标服务 服务无
  13. 背景文件上传是 Java Web 项目里很常见的功能,但也是代码审计中高频出问题的位置。很多漏洞并不是因为开发完全没有做校验,而是校验点放错了、信任了客户端参数,或者只处理了扩展名却忽略了存储路径、内容类型和访问控制。 这篇文章整理一个偏通用的审计思路,重点放在如何发现风险、如何验证风险边界,以及如何给出可落地的修复方案。内容不会涉及真实站点攻击,只讨论本地测试环境和代码层面的安全检查。 典型场景常见业务包括头像上传、附件上传、富文本图片上传、工单附件、导入文件等。审计时我通常会先关注以下几个点: 上传文件是否直接保存到 Web 可访问目录。文件名是否直接使用用户传入值。扩展名校验是否只依赖前端或 Content-Type。是否允许上传 HTML、SVG、JSP、jspx、jspx 等高风险类型。是否存在路径拼接导致的目录穿越。上传后的文件是否有独立访问鉴权。是否对文件大小、数量、解压内容做限制。很多项目表面上做了白名单,但细看代码会发现只对原始文件名做了字符串判断,保存时又从其他参数取文件名,导致校验和落盘对象不是同一个。 问题代码示例下面是一段简化后的示例代码,便于说明问题。不要直接套用到生产环境。 @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file, @RequestParam("name") String name) throws IOException { String originalName = file.getOriginalFilename(); if (originalName == null || !originalName.endsWith(".jpg")) { return "invalid file"; } File dir = new File("/data/app/static/upload/"); if (!dir.exists()) { dir.mkdirs(); } File target = new File(dir, name); file.transferTo(target); return "/upload/" + name; }这段代码有几个明显风险: 校验的是 originalName,但保存使用的是 name 参数,二者不一致。只用 endsWith 校验扩展名,大小写、特殊后缀、双扩展名等情况都容易漏掉。保存目录位于静态资源目录,上传后可直接访问。name 未做规范化处理,存在路径穿越和覆盖已有文件的风险。没有校验真实文件类型,也没有限制文件大小。审计时的分析方法做代码审计时,不建议只搜 upload 或 MultipartFile 就结束。更稳妥的方式是从数据流角度看完整链路: 入口:MultipartFile、ServletInputStream、Commons FileUpload、Base64 文件内容。校验:扩展名、MIME、魔数、大小、图片解析、业务类型。命名:是否使用用户输入、是否随机化、是否保留原始名称。存储:本地目录、对象存储、NFS、临时目录。访问:静态映射、下载接口、鉴权、Content-Disposition。后处理:图片压缩、文档解析、压缩包解压、异步扫描。尤其要注意“校验对象”和“保存对象”是否一致。如果校验 file.getOriginalFilename(),但保存路径来自 request 参数、数据库字段或前端传入的 key,就要重点看是否能被用户控制。 关键验证点在本地测试环境中,可以围绕以下几个方向验证风险,不需要对外部系统做任何操作。 1. 扩展名白名单是否严格推荐使用服务端固定白名单,并统一转小写后判断。不要使用黑名单,因为遗漏成本很高。 private static final Set<String> ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "gif", "pdf"); private boolean isAllowedExt(String filename) { if (filename == null) { return false; } String cleanName = Paths.get(filename).getFileName().toString(); int idx = cleanName.lastIndexOf('.'); if (idx <= 0 || idx == cleanName.length() - 1) { return false; } String ext = cleanName.substring(idx + 1).toLowerCase(Locale.ROOT); return ALLOWED_EXT.contains(ext); }这里的重点不是“能不能拦住所有情况”,而是先保证判断逻辑清晰,且只允许业务确实需要的类型。 2. 文件名是否可控上传后的文件名建议由服务端生成,原始文件名只作为展示字段保存,不能参与实际路径拼接。 String ext = getSafeExt(file.getOriginalFilename()); String storedName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path baseDir = Paths.get("/data/app/uploads").toAbsolutePath().normalize(); Path target = baseDir.resolve(storedName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("invalid path"); } Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);即使文件名由服务端生成,也建议保留 normalize 和 startsWith 检查。这类防御看起来重复,但能减少后续维护人员改动代码时引入路径问题。 3. 是否校验文件内容Content-Type 是客户端传入值,不能直接信任。对于图片类上传,可以结合魔数识别和图片解码。对于 PDF、Office 等文档,可以做基本类型识别,并配合杀毒或沙箱扫描。 private boolean isPng(byte[] header) { byte[] png = new byte[] {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; return header.length >= png.length && Arrays.equals(Arrays.copyOf(header, png.length), png); }如果业务只允许图片,建议上传后重新编码生成新图片,而不是原样保存。这样可以在一定程度上去掉多余内容和异常结构。 4. 上传目录是否可执行或可直接访问文件尽量存储在 Web 根目录之外,通过受控下载接口访问。下载接口要做鉴权,并设置合适响应头,避免浏览器直接解析高风险内容。 response.setHeader("Content-Disposition", "attachment; filename=\"" + safeDownloadName + "\""); response.setHeader("X-Content-Type-Options", "nosniff"); response.setContentType("application/octet-stream");如果必须使用 Nginx 暴露静态文件,也要确保该目录没有脚本执行能力,并限制危险类型。 location /uploads/ { alias /data/app/uploads/; autoindex off; add_header X-Content-Type-Options nosniff; types { } default_type application/octet-stream; }压缩包上传的额外风险很多系统允许上传 zip 包后自动解压,这类功能风险更高。除了文件类型校验,还要关注 Zip Slip、解压炸弹、文件数量过多、嵌套目录过深等问题。 安全解压时至少需要做以下限制: 每个条目解压后的路径必须仍在目标目录内。限制压缩包总大小和解压后总大小。限制文件数量和目录层级。禁止符号链接、绝对路径和特殊设备文件。解压后的每个文件仍要按业务白名单检查。Path destDir = Paths.get("/data/app/unzip").toAbsolutePath().normalize(); try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { Path out = destDir.resolve(entry.getName()).normalize(); if (!out.startsWith(destDir)) { throw new SecurityException("zip entry path traversal"); } // 后续再处理大小、数量、类型等限制 } }修复建议清单风险点建议做法扩展名绕过使用服务端白名单,统一大小写,禁止危险类型路径穿越服务端生成文件名,路径 normalize 后校验 startsWith文件内容伪装结合魔数、文件解析、重新编码或安全扫描上传后直接访问存储到 Web 根目录外,通过鉴权接口下载浏览器解析风险设置 Content-Disposition 和 X-Content-Type-Options大文件消耗资源限制单文件大小、总量、上传频率和超时时间压缩包风险限制路径、数量、层级、解压后大小和文件类型常见误区只做前端校验:前端校验只能提升体验,不能作为安全边界。只判断 Content-Type:该字段可被客户端控制,最多作为辅助信息。只用黑名单:危险扩展名和容器行为太多,容易漏。认为对象存储一定安全:对象存储也要关注公开读、Content-Type、下载鉴权和生命周期。忽略业务权限:上传安全不只是不执行脚本,还包括越权读取他人附件。总结文件上传漏洞的核心问题通常不是单个 if 写错,而是整条链路缺少一致的安全设计。比较稳的做法是:服务端生成文件名、严格白名单、存储在非 Web 根目录、受控下载、内容校验和资源限制一起做。 审计时建议把上传功能当作一条数据流看,从入口到访问再到后处理逐段确认。这样不但能发现明显漏洞,也能发现那些在重构、迁移对象存储、增加压缩包导入功能时容易被带出来的隐患。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 项目 目里 里很 很常 常见 见的 的功 功能 项目里 目里很 里很常 很常见 常见的 见的功 的功能 项目里很 目里很常 里很常见 很常见的 常见的功 见的功能 但也 也是 是代 代码 码审 审计 计中 中高 高频 频出 出问 问题 题的 的位 位置 但也是 也是代 是代码 代码审 码审计 审计中 计中高 中高频 高频出 频出问 出问题 问题的 题的位 的位置 但也是代 也是代码 是代码审 代码审计 码审计中 审计中高 计中高频 中高频出 高频出问 频出问题 出问题的 问题的位 题的位置 很多 多漏 漏洞 洞并 并不 不是 是因 因为 为开 开发 发完 完全 全没 没有 有做 做校 校验 很多漏 多漏洞 漏洞并 洞并不 并不是 不是因 是因为 因为开 为开发 开发完 发完全 完全没 全没有 没有做 有做校 做校验 很多漏洞 多漏洞并 漏洞并不 洞并不是 并不是因 不是因为 是因为开 因为开发 为开发完 开发完全 发完全没 完全没有 全没有做 没有做校 有做校验 而是 是校 验点 点放 放错 错了 而是校 是校验 校验点 验点放 点放错 放错了 而是校验 是校验点 校验点放 验点放错 点放错了 信任 任了 了客 客户 户端 端参 参数 信任了 任了客 了客户 客户端 户端参 端参数 信任了客 任了客户 了客户端 客户端参 户端参数 或者 者只 只处 处理 理了 了扩 扩展 展名 名却 却忽 忽略 略了 了存 存储 储路 路径 或者只 者只处 只处理 处理了 理了扩 了扩展 扩展名 展名却 名却忽 却忽略 忽略了 略了存 了存储 存储路 储路径 或者只处 者只处理 只处理了 处理了扩 理了扩展 了扩展名 扩展名却 展名却忽 名却忽略 却忽略了 忽略了存 略了存储 了存储路 存储路径 内容 容类 类型 型和 和访 访问 问控 控制 内容类 容类型 类型和 型和访 和访问 访问控 问控制 内容类型 容类型和 类型和访 型和访问 和访问控 访问控制 这篇 篇文 文章 章整 整理 理一 一个 个偏 偏通 通用 用的 的审 计思 思路 这篇文 篇文章 文章整 章整理 整理一 理一个 一个偏 个偏通 偏通用 通用的 用的审 的审计 审计思 计思路 这篇文章 篇文章整 文章整理 章整理一 整理一个 理一个偏 一个偏通 个偏通用 偏通用的 通用的审 用的审计 的审计思 审计思路 重点 放在 在如 如何 何发 发现 现风 风险 重点放 点放在 放在如 在如何 如何发 何发现 发现风 现风险 重点放在 点放在如 放在如何 在如何发 如何发现 何发现风 发现风险 何验 验证 证风 险边 边界 如何验 何验证 验证风 证风险 风险边 险边界 如何验证 何验证风 验证风险 证风险边 风险边界 以及 及如 何给 给出 出可 可落 落地 地的 的修 修复 复方 方案 以及如 及如何 如何给 何给出 给出可 出可落 可落地 落地的 地的修 的修复 修复方 复方案 以及如何 及如何给 如何给出 何给出可 给出可落 出可落地 可落地的 落地的修 地的修复 的修复方 修复方案 容不 不会 会涉 涉及 及真 真实 实站 站点 点攻 攻击 内容不 容不会 不会涉 会涉及 涉及真 及真实 真实站 实站点 站点攻 点攻击 内容不会 容不会涉 不会涉及 会涉及真 涉及真实 及真实站 真实站点 实站点攻 站点攻击 只讨 讨论 论本 本地 地测 测试 试环 环境 境和 和代 码层 层面 面的 的安 安全 全检 检查 只讨论 讨论本 论本地 本地测 地测试 测试环 试环境 环境和 境和代 和代码 代码层 码层面 层面的 面的安 的安全 安全检 全检查 只讨论本 讨论本地 论本地测 本地测试 地测试环 测试环境 试环境和 环境和代 境和代码 和代码层 代码层面 码层面的 层面的安 面的安全 的安全检 安全检查 典型 型场 场景 景常 见业 业务 务包 包括 括头 头像 像上 典型场 型场景 场景常 景常见 常见业 见业务 业务包 务包括 包括头 括头像 头像上 像上传 典型场景 型场景常 场景常见 景常见业 常见业务 见业务包 业务包括 务包括头 包括头像 括头像上 头像上传 附件 附件上 附件上传 富文 文本 本图 图片 片上 富文本 文本图 本图片 图片上 片上传 富文本图 文本图片 本图片上 图片上传 工单 单附 工单附 单附件 工单附件 导入 入文 件等 导入文 入文件 文件等 导入文件 入文件等 计时 时我 我通 通常 常会 会先 先关 关注 注以 以下 下几 几个 个点 审计时 计时我
  14. 背景近几年在代码审计和应急处置里,遇到的供应链投毒样本明显多了起来。它们不一定追求复杂的漏洞利用,更多是借助开发者信任链:包管理器、构建脚本、CI 环境变量、发布流水线等。一旦开发或构建环境被读取到敏感信息,影响范围往往比单点 Web 漏洞更大。 这篇文章整理一次典型投毒样本的静态分析过程,重点放在识别思路、防护点和排查方法上。文中不提供可直接复用的攻击代码,也不讨论绕过检测或持久化控制,只讨论如何判断风险、定位行为和做工程侧加固。 样本场景样本来自一次内部依赖排查:某个看似正常的 npm 包在安装阶段触发异常网络访问。包名、域名和哈希这里都做了脱敏处理。初步现象包括: 安装依赖时执行了额外脚本。脚本读取了环境变量、用户目录下的配置文件。存在对外 HTTP 请求,目标域名与项目业务无关。包的公开描述与代码实际功能明显不匹配。这类样本的关键不在于“技术多高级”,而在于它混进了开发流程。很多团队对线上流量有监控,但对 CI Runner、开发机和构建容器的出站行为监控并不充分。 技术分析思路分析供应链样本时,我一般按以下顺序做静态检查: 先看包元信息:package.json、安装脚本、入口文件、依赖列表。再看执行链路:preinstall、install、postinstall、prepare 等生命周期脚本。检查可疑能力:文件读取、环境变量读取、进程执行、网络请求、编码混淆。最后做影响面判断:可能读取哪些凭据、触发条件是什么、是否只在特定系统执行。以 npm 包为例,package.json 是优先关注点。生命周期脚本经常被滥用,因为开发者执行 npm install 时很容易忽略它们。 { "name": "example-helper", "version": "1.2.3", "main": "index.js", "scripts": { "postinstall": "node ./scripts/setup.js" } }正常包也会使用 postinstall,比如编译原生扩展、下载平台相关资源等。但如果一个简单工具包在安装阶段读取用户目录、访问网络、执行系统命令,就需要提高警惕。 关键文件定位本次样本的入口在 scripts/setup.js。文件做了简单变量拆分和字符串拼接,没有复杂加壳,但足以绕过只看关键字的粗糙检查。整理后可以看到几个典型动作: 读取 process.env。枚举用户主目录下的若干配置文件。将结果组装为 JSON。发起外连请求。安全分析时不需要还原成可运行的攻击代码,只要确认行为边界即可。可以用关键词辅助定位: grep -R "process.env\|require('fs')\|require(\"fs\")\|http.request\|https.request\|child_process" -n .如果仓库较大,也可以先把压缩和构建产物排除掉: grep -R "process.env\|child_process\|https.request" -n . \ --exclude-dir=node_modules \ --exclude-dir=dist \ --exclude-dir=build实际排查时,除了显式 require,还要注意动态导入和间接调用。例如通过字符串拼接得到模块名,或者把网络请求封装在第三方依赖里。遇到明显混淆时,可以优先关注以下模式: 大量无意义变量名和字符串数组。Buffer.from、atob、fromCharCode 等编码还原逻辑。eval、Function 构造器、vm.runInNewContext。异常捕获后静默退出,避免安装失败引起注意。敏感信息读取点供应链投毒最常见的目标是开发和构建环境里的凭据。需要重点关注以下位置: 位置风险说明环境变量CI Token、云厂商 AK/SK、Webhook、数据库连接串可能在其中~/.npmrc可能包含私有源 token~/.git-credentials可能包含 Git 凭据,风险较高~/.ssh私钥读取属于高危行为,应立即隔离分析项目 .env常见于后端服务配置,容易包含密钥和连接串需要强调一点:发现样本尝试读取这些文件,并不等于已经成功泄露。最终还要结合执行环境、权限、网络出口、日志证据判断。但从响应角度看,只要存在读取和外连行为,就应按凭据暴露处理。 网络行为判断静态分析中,网络目标通常会被写死、拆分或编码。可以从以下几类线索入手: http、https、dns、net、tls 相关模块调用。URL、域名、IP、路径片段字符串。base64 或十六进制编码后的字符串。请求头中伪装成正常统计、错误上报或版本检查的字段。对于 CI 环境,建议保留安装依赖阶段的出站连接日志。没有日志时,很难确认是否发生过数据外传。工程上可以通过代理或防火墙策略限制构建容器只访问必要源,例如包仓库、制品库和代码仓库。 # 示例:在 Linux 环境中查看近期可疑连接记录,具体路径依发行版和审计配置而定 journalctl --since "2 hours ago" | grep -Ei "npm|node|curl|wget|https"如果有统一出口代理,优先查代理日志。重点关注构建任务启动前后几分钟内的未知域名、低信誉域名、非业务域名。 本地复现的安全边界样本分析不建议直接在办公机或真实 CI 上执行。更稳妥的方式是隔离环境静态分析,必要时使用无敏感信息、无外网或受控网络的沙箱环境观察行为。 可执行的安全做法包括: 使用临时虚拟机或容器,不挂载真实用户目录。禁用或限制外网,只允许访问内部抓包代理。准备空的 HOME 目录,避免真实凭据被读取。执行前后对文件系统和进程行为做快照对比。不要把真实 token、私钥、配置文件放进复现环境。如果只是确认安装脚本是否存在,可以使用忽略脚本参数完成依赖安装: npm install --ignore-scripts在排查未知包时,这是一个很实用的习惯。pnpm、yarn 也有类似能力,团队可以在安全基线里统一要求。 代码审计关注点这类样本在代码层面经常有一些共性,不一定每条都恶意,但组合出现时风险会明显升高: 小工具包却包含安装阶段脚本。安装阶段读取 HOME、SSH、npm、git 配置。异常处理全部吞掉,没有日志。根据操作系统、用户名、主机名、CI 标识决定是否执行。通过编码、压缩、字符串拼接隐藏真实域名和路径。依赖包发布时间很新,但下载量短期异常增长。包名与知名库相似,存在拼写混淆。审计时可以把“安装期执行”和“运行期执行”分开看。运行期代码至少还需要业务调用路径触发,而安装期脚本往往在依赖安装时自动执行,风险更隐蔽。 应急处置建议如果确认项目依赖中存在投毒包,建议按下面顺序处理: 暂停相关 CI 任务,保留构建日志、依赖锁文件和制品。隔离执行过安装流程的 Runner、构建容器和开发机。确认包版本范围,排查 lock 文件中实际解析到的版本。检查出站访问日志,确认是否访问过可疑域名或 IP。轮换可能暴露的凭据,包括 npm token、Git token、云密钥、Webhook。清理缓存中的恶意包,避免后续构建继续命中。将依赖固定到安全版本,并通过内部源或制品库做准入。这里不要只删除 package.json 里的依赖。很多情况下 lock 文件、包管理器缓存、CI 缓存仍然会保留旧版本。应急时要同时处理缓存和制品。 # 示例:查看 lock 文件中实际使用的包版本 npm ls example-helper # 清理 npm 本地缓存需结合团队策略执行 npm cache verify如果确认凭据可能被读取,轮换密钥比争论“是否一定泄露”更重要。尤其是长期有效、权限较大的 CI Token 和云访问密钥,应优先处理。 工程侧防护供应链风险很难靠个人谨慎完全解决,需要在工程流程中设置防线: 启用 lock 文件审查,避免依赖版本漂移。CI 默认使用 npm install --ignore-scripts 或在白名单包上允许脚本。构建容器最小权限运行,不挂载宿主机敏感目录。CI 凭据按项目最小授权,避免组织级高权限 token 下发到普通任务。限制构建环境出站访问,只允许必要的包源和内部服务。内部制品库做包准入和缓存,减少直接拉取公网包。对新增依赖、维护者变更、安装脚本变更做自动告警。很多团队会做 SCA 扫描,但默认只关注已公开漏洞编号。供应链投毒往往没有 CVE,也不一定能被传统漏洞库命中。因此还需要行为规则:安装脚本、外连、敏感文件读取、混淆代码等。 几个容易忽略的点devDependencies 也有风险。CI 安装开发依赖时同样会执行脚本。prepare 脚本在某些安装场景也会触发,不能只看 postinstall。删除依赖不等于删除影响,已经泄露的 token 仍需轮换。私有源不等于绝对安全,内部包也可能被账号接管或发布流程污染。不要在真实开发机上运行未知样本,尤其不要带着个人 SSH 和云凭据分析。总结供应链投毒的核心问题不是单个脚本多复杂,而是它利用了开发流程中的默认信任。对防守方来说,长期有效的做法是把依赖安装、构建环境、凭据管理和网络出口都纳入安全边界。 实际落地可以先从三件事开始:依赖安装默认限制脚本、CI 凭据最小化、构建环境出站访问可观测。做到这三点,即使遇到类似投毒样本,也能明显降低影响范围,并且更快完成定位和处置。 漏洞成因、验证条件与修复闭环示意 背景 景近 近几 几年 年在 在代 代码 码审 审计 计和 和应 应急 急处 处置 置里 背景近 景近几 近几年 几年在 年在代 在代码 代码审 码审计 审计和 计和应 和应急 应急处 急处置 处置里 背景近几 景近几年 近几年在 几年在代 年在代码 在代码审 代码审计 码审计和 审计和应 计和应急 和应急处 应急处置 急处置里 遇到 到的 的供 供应 应链 链投 投毒 毒样 样本 本明 明显 显多 多了 了起 起来 遇到的 到的供 的供应 供应链 应链投 链投毒 投毒样 毒样本 样本明 本明显 明显多 显多了 多了起 了起来 遇到的供 到的供应 的供应链 供应链投 应链投毒 链投毒样 投毒样本 毒样本明 样本明显 本明显多 明显多了 显多了起 多了起来 它们 们不 不一 一定 定追 追求 求复 复杂 杂的 的漏 漏洞 洞利 利用 它们不 们不一 不一定 一定追 定追求 追求复 求复杂 复杂的 杂的漏 的漏洞 漏洞利 洞利用 它们不一 们不一定 不一定追 一定追求 定追求复 追求复杂 求复杂的 复杂的漏 杂的漏洞 的漏洞利 漏洞利用 更多 多是 是借 借助 助开 开发 发者 者信 信任 任链 更多是 多是借 是借助 借助开 助开发 开发者 发者信 者信任 信任链 更多是借 多是借助 是借助开 借助开发 助开发者 开发者信 发者信任 者信任链 包管 管理 理器 包管理 管理器 包管理器 构建 建脚 脚本 构建脚 建脚本 构建脚本 环境 境变 变量 环境变 境变量 环境变量 发布 布流 流水 水线 线等 发布流 布流水 流水线 水线等 发布流水 布流水线 流水线等 一旦 旦开 发或 或构 建环 境被 被读 读取 取到 到敏 敏感 感信 信息 一旦开 旦开发 开发或 发或构 或构建 构建环 建环境 环境被 境被读 被读取 读取到 取到敏 到敏感 敏感信 感信息 一旦开发 旦开发或 开发或构 发或构建 或构建环 构建环境 建环境被 环境被读 境被读取 被读取到 读取到敏 取到敏感 到敏感信 敏感信息 影响 响范 范围 围往 往往 往比 比单 单点 影响范 响范围 范围往 围往往 往往比 往比单 比单点 影响范围 响范围往 范围往往 围往往比 往往比单 往比单点 洞更 更大 漏洞更 洞更大 漏洞更大 这篇 篇文 文章 章整 整理 理一 一次 次典 典型 型投 本的 的静 静态 态分 分析 析过 过程 这篇文 篇文章 文章整 章整理 整理一 理一次 一次典 次典型 典型投 型投毒 样本的 本的静 的静态 静态分 态分析 分析过 析过程 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次典 一次典型 次典型投 典型投毒 型投毒样 毒样本的 样本的静 本的静态 的静态分 静态分析 态分析过 分析过程 重点 点放 放在 在识 识别 别思 思路 重点放 点放在 放在识 在识别 识别思 别思路 重点放在 点放在识 放在识别 在识别思 识别思路 防护 护点 点和 和排 排查 查方 方法 法上 防护点 护点和 点和排 和排查 排查方 查方法 方法上 防护点和 护点和排 点和排查 和排查方 排查方法 查方法上 文中 中不 不提 提供 供可 可直 直接 接复 复用 用的 的攻 攻击 击代 文中不 中不提 不提供 提供可 供可直 可直接 直接复 接复用 复用的 用的攻 的攻击 攻击代 击代码 文中不提 中不提供 不提供可 提供可直 供可直接 可直接复 直接复用 接复用的 复用的攻 用的攻击 的攻击代 攻击代码 也不 不讨 讨论 论绕 绕过 过检 检测 测或 或持 持久 久化 化控 控制 也不讨 不讨论 讨论绕 论绕过 绕过检 过检测 检测或 测或持 或持久 持久化 久化控 化控制 也不讨论 不讨论绕 讨论绕过 论绕过检 绕过检测 过检测或 检测或持 测或持久 或持久化 持久化控 久化控制 只讨 论如 如何 何判 判断 断风 风险 只讨论 讨论如 论如何 如何判 何判断 判断风 断风险 只讨论如 讨论如何 论如何判 如何判断 何判断风 判断风险 定位 位行 行为 为和 和做 做工 工程 程侧 侧加 加固 定位行 位行为 行为和 为和做 和做工 做工程 工程侧 程侧加 侧加固 定位行为 位行为和 行为和做 为和做工 和做工程 做工程侧 工程侧加 程侧加固 本场 场景 景样 本来 来自 自一 次内 内部 部依 依赖 赖排 样本场 本场景 场景样 景样本 样本来 本来自 来自一 自一次 一次内 次内部 内部依 部依赖 依赖排 赖排查 样本场景 本场景样 场景样本 景样本来 样本来自 本来自一 来自一次 自一次内 一次内部 次内部依 内部依赖 部依赖排 依赖排查 某个 个看 看似 似正 正常 常的
  15. 背景很多 Web 入侵事件并不是一开始就能看到明确的 WebShell 或异常进程,更多时候只是访问日志里出现了几个奇怪的请求:异常路径、非常规 User-Agent、短时间高频 404、对历史漏洞路径的批量探测等。对运维和安全人员来说,能不能从这些碎片里快速判断风险级别,往往决定了后续处置是否及时。 这篇文章整理一套我平时做 Web 攻击面排查时比较常用的方法,重点放在 Nginx 访问日志、应用日志和主机侧痕迹的交叉分析。内容偏防守和复盘,不涉及未授权利用,也不会提供攻击载荷,只讨论如何识别、验证和加固。 场景描述某业务系统上线多年,前端由 Nginx 反向代理,后端是 Java 应用。某天监控发现 404 请求量短时间内升高,同时有少量请求命中了敏感路径规则。业务没有明显异常,但从经验看,这类情况通常有几种可能: 互联网扫描器批量探测常见漏洞路径。针对特定组件版本的漏洞探测。业务自身存在历史遗留接口,被外部误扫或撞库式访问。攻击者已经掌握部分目录结构,正在做进一步枚举。排查目标不是“证明一定被攻击”,而是回答几个问题:请求从哪里来、访问了什么、是否命中真实组件、是否造成状态变化、是否留下持久化痕迹。 第一步:先把日志切出可分析范围排查时不要一上来就全量 grep,容易被噪声淹没。建议先按时间窗口、状态码、路径特征做粗筛。假设 Nginx 日志路径为 /var/log/nginx/access.log,可以先看异常状态码分布: awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head如果已经知道异常时间段,可以按时间过滤。不同环境日志格式不同,下面只是常见格式示例: grep '10/Mar/2026:14:' /var/log/nginx/access.log | awk '{print $1,$6,$7,$9,$10,$12}' | head -50再统计访问路径,找出高频异常路径: grep '10/Mar/2026:14:' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -50如果发现大量类似后台路径、配置文件路径、备份文件后缀、框架默认接口等请求,通常可以先按扫描处理,但仍需要确认是否命中了真实资源。 第二步:识别扫描噪声和有效探测日志里最常见的是通用扫描噪声,例如访问不存在的管理入口、历史漏洞路径、压缩包和数据库备份文件名等。判断是否值得深入,可以看几个维度: 状态码:大量 404 更偏扫描;200、302、401、403 需要重点看。响应大小:同一路径不同请求响应大小异常变化,可能说明参数触发了不同逻辑。请求方法:POST、PUT、PATCH、DELETE 比单纯 GET 更值得关注。来源 IP:单 IP 慢速访问业务接口,和多 IP 批量扫路径,性质不同。User-Agent:明显工具特征不一定代表高危,但可作为聚类线索。Referer:空 Referer 不一定异常,但与敏感路径组合时要关注。可以用下面的方式统计某个可疑 IP 的访问行为: grep '^203.0.113.10 ' /var/log/nginx/access.log | awk '{print $4,$5,$6,$7,$9,$10,$12}' | head -100也可以按 IP 聚合访问量和状态码,判断是否存在批量探测: awk '{count[$1" "$9]++} END {for (i in count) print count[i], i}' /var/log/nginx/access.log | sort -nr | head -50这里建议不要只依赖单条日志下结论。很多真实事件里,最关键的不是“访问了某个敏感路径”,而是“访问敏感路径之后,又访问了一个业务接口,并且状态码从 404 变成了 200”。 第三步:确认是否命中真实组件如果日志中出现某类组件的典型路径,下一步是确认业务是否真的使用该组件,以及暴露范围是否符合预期。排查思路一般是: 查 Nginx 配置,确认 location 转发关系和静态目录映射。查应用依赖,确认是否包含相关组件或历史版本。查路由配置,确认接口是否需要鉴权。查发布目录,确认是否存在测试文件、备份文件、旧版本包。查看 Nginx 有效配置时,建议使用: nginx -T 2>/dev/null | less重点关注这些配置: root、alias 是否指向了不该暴露的目录。location 匹配顺序是否导致鉴权被绕过。是否存在临时开放的调试路径。是否允许上传目录执行脚本。反向代理是否把内部管理端口暴露到公网。Java 应用可以从构建文件或部署包确认依赖版本。例如 Maven 项目: grep -R "artifactId\|version" pom.xml */pom.xml 2>/dev/null | head -100如果是已经打包的应用,也可以在解压后的 WEB-INF/lib 或容器部署目录中查看 jar 包名称。这里不建议直接在生产环境做破坏性验证,更合适的方式是拉取同版本到隔离环境复现,或通过只读方式确认版本和配置。 第四步:检查是否存在状态变化很多探测请求本身不会造成影响,真正需要警惕的是后续是否出现了状态变化。可以围绕以下几类痕迹排查: 文件变化:Web 目录、上传目录、临时目录是否新增异常文件。进程变化:是否出现异常子进程、非常规命令调用。网络连接:是否有应用进程发起异常外连。账号变化:是否新增系统账号、应用后台账号、访问令牌。计划任务:是否出现异常 cron、systemd timer 或启动项。查看最近变更的 Web 文件: find /var/www /data/www -type f -mtime -3 -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | sort查看上传目录中可疑脚本文件: find /data/uploads -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.jspx' -o -name '*.asp' -o -name '*.aspx' \) 2>/dev/null查看应用用户下的异常进程: ps -eo user,pid,ppid,lstart,cmd --sort=start_time | tail -80查看当前连接,重点看应用进程是否有不符合业务预期的外连: ss -antp | head -100如果发现可疑文件,不建议立即删除。更稳妥的做法是先隔离、备份、记录时间戳和哈希,便于后续溯源和确认影响范围: sha256sum suspicious_file stat suspicious_file第五步:关联应用日志和业务行为Nginx 日志只能看到入口请求,很多关键细节在应用日志里。例如鉴权失败、参数校验异常、反序列化报错、模板解析错误、SQL 异常、文件上传失败等。排查时可以用访问日志中的时间点、请求 ID、客户端 IP 去应用日志里交叉检索。 如果业务已经接入统一日志,建议按以下字段查询: 请求时间前后 5 到 10 分钟。客户端 IP 或经过代理后的真实 IP。URI、接口名、控制器方法。登录用户、租户 ID、角色信息。异常堆栈和错误码。一个常见误区是只看 Web 容器 access log,不看业务审计日志。比如某个请求返回 200,可能只是页面正常返回;也可能已经触发了配置修改、文件导入、任务创建等业务动作。对管理后台、文件管理、任务调度、插件市场这类功能,要重点看操作审计。 关键加固建议排查结束后,应该把发现的问题固化成配置和流程,而不是只封几个 IP。比较实用的加固项包括: 公网只暴露必要路径,管理后台放到 VPN、零信任或内网访问范围内。关闭默认示例页面、调试接口、历史测试目录。上传目录禁止脚本执行,静态资源域名与应用执行环境隔离。Nginx 对敏感后缀和隐藏文件增加拒绝规则,如 .git、.svn、备份文件等。应用依赖建立版本清单,定期比对安全公告。关键接口增加操作审计,并确保日志包含请求 ID、用户 ID 和来源 IP。对 404 暴增、敏感路径访问、异常 POST 请求建立告警,而不是只看 5xx。一个基础的 Nginx 防护配置示例: location ~ /\. { deny all; } location ~* \.(bak|old|orig|save|swp|zip|tar|gz|sql)$ { deny all; } location ^~ /uploads/ { types { } default_type application/octet-stream; add_header X-Content-Type-Options nosniff; }注意,这类配置需要结合业务验证,避免误伤正常下载功能。配置变更前建议先在测试环境回放关键路径,或至少通过灰度方式上线。 事件记录模板做排查时建议保留结构化记录,后续复盘会轻松很多。一个简单模板如下: 项目记录内容发现时间告警触发时间、人工发现时间影响资产域名、服务器、应用、容器、版本异常现象异常路径、状态码、请求量、来源 IP验证结果是否命中真实组件、是否鉴权、是否状态变化处置动作临时封禁、配置修复、版本升级、隔离文件遗留风险待升级组件、待补充日志、待收敛入口注意点不要因为来源 IP 是云厂商或海外地址就直接判定为攻击成功,扫描和成功入侵是两回事。不要只看单个请求,要看请求序列。真实攻击通常会有探测、验证、操作、清理等阶段性特征。不要在生产环境执行不确定的验证脚本,尤其是会写文件、发请求、触发任务的操作。发现可疑文件时先留证再处理,避免破坏时间线。如果涉及用户数据、凭证或后台权限,应按内部应急流程升级,不要只在机器上做局部清理。总结Web 攻击面排查的核心不是记住多少漏洞路径,而是建立一套稳定的判断链路:入口日志发现异常,配置和依赖确认暴露面,应用日志验证业务影响,主机侧痕迹确认是否落地,最后再做收敛和加固。 很多事件最后复盘下来,问题不在于没有设备或没有日志,而是日志字段不全、配置长期漂移、旧接口没人维护。把这些基础工作补齐,比临时堆规则更有长期价值。 代码审计、调用链与关键函数定位示意 背景 景很 很多 背景很 景很多 背景很多 入侵 侵事 事件 件并 并不 不是 是一 一开 开始 始就 就能 能看 看到 到明 明确 确的 入侵事 侵事件 事件并 件并不 并不是 不是一 是一开 一开始 开始就 始就能 就能看 能看到 看到明 到明确 明确的 入侵事件 侵事件并 事件并不 件并不是 并不是一 不是一开 是一开始 一开始就 开始就能 始就能看 就能看到 能看到明 看到明确 到明确的 或异 异常 常进 进程 或异常 异常进 常进程 或异常进 异常进程 更多 多时 时候 候只 只是 是访 访问 问日 日志 志里 里出 出现 现了 了几 几个 个奇 奇怪 怪的 的请 请求 更多时 多时候 时候只 候只是 只是访 是访问 访问日 问日志 日志里 志里出 里出现 出现了 现了几 了几个 几个奇 个奇怪 奇怪的 怪的请 的请求 更多时候 多时候只 时候只是 候只是访 只是访问 是访问日 访问日志 问日志里 日志里出 志里出现 里出现了 出现了几 现了几个 了几个奇 几个奇怪 个奇怪的 奇怪的请 怪的请求 常路 路径 异常路 常路径 异常路径 非常 常规 非常规 短时 时间 间高 高频 短时间 时间高 间高频 短时间高 时间高频 对历 历史 史漏 漏洞 洞路 径的 的批 批量 量探 探测 测等 对历史 历史漏 史漏洞 漏洞路 洞路径 路径的 径的批 的批量 批量探 量探测 探测等 对历史漏 历史漏洞 史漏洞路 漏洞路径 洞路径的 路径的批 径的批量 的批量探 批量探测 量探测等 对运 运维 维和 和安 安全 全人 人员 员来 来说 对运维 运维和 维和安 和安全 安全人 全人员 人员来 员来说 对运维和 运维和安 维和安全 和安全人 安全人员 全人员来 人员来说 能不 不能 能从 从这 这些 些碎 碎片 片里 里快 快速 速判 判断 断风 风险 险级 级别 能不能 不能从 能从这 从这些 这些碎 些碎片 碎片里 片里快 里快速 快速判 速判断 判断风 断风险 风险级 险级别 能不能从 不能从这 能从这些 从这些碎 这些碎片 些碎片里 碎片里快 片里快速 里快速判 快速判断 速判断风 判断风险 断风险级 风险级别 往往 往决 决定 定了 了后 后续 续处 处置 置是 是否 否及 及时 往往决 往决定 决定了 定了后 了后续 后续处 续处置 处置是 置是否 是否及 否及时 往往决定 往决定了 决定了后 定了后续 了后续处 后续处置 续处置是 处置是否 置是否及 是否及时 这篇 篇文 文章 章整 整理 理一 一套 套我 我平 平时 时做 这篇文 篇文章 文章整 章整理 整理一 理一套 一套我 套我平 我平时 平时做 这篇文章 篇文章整 文章整理 章整理一 整理一套 理一套我 一套我平 套我平时 我平时做 攻击 击面 面排 排查 查时 时比 比较 较常 常用 用的 的方 方法 攻击面 击面排 面排查 排查时 查时比 时比较 比较常 较常用 常用的 用的方 的方法 攻击面排 击面排查 面排查时 排查时比 查时比较 时比较常 比较常用 较常用的 常用的方 用的方法 重点 点放 放在 重点放 点放在 重点放在 应用 用日 志和 和主 主机 机侧 侧痕 痕迹 迹的 的交 交叉 叉分 分析 应用日 用日志 日志和 志和主 和主机 主机侧 机侧痕 侧痕迹 痕迹的 迹的交 的交叉 交叉分 叉分析 应用日志 用日志和 日志和主 志和主机 和主机侧 主机侧痕 机侧痕迹 侧痕迹的 痕迹的交 迹的交叉 的交叉分 交叉分析 内容 容偏 偏防 防守 守和 和复 复盘 内容偏 容偏防 偏防守 防守和 守和复 和复盘 内容偏防 容偏防守 偏防守和 防守和复 守和复盘 不涉 涉及 及未 未授 授权 权利 利用 不涉及 涉及未 及未授 未授权 授权利 权利用 不涉及未 涉及未授 及未授权 未授权利 授权利用 也不 不会 会提 提供 供攻 击载 载荷 也不会 不会提 会提供 提供攻 供攻击 攻击载 击载荷 也不会提 不会提供 会提供攻 提供攻击 供攻击载 攻击载荷 只讨 讨论 论如 如何 何识 识别 只讨论 讨论如 论如何 如何识 何识别 只讨论如 讨论如何 论如何识 如何识别 验证 证和 和加 加固 验证和 证和加 和加固 验证和加 证和加固 场景 景描 描述 述某 某业 业务 务系 系统 统上 上线 线多 多年 场景描 景描述 描述某 述某业 某业务 业务系 务系统 系统上 统上线 上线多 线多年 场景描述 景描述某 描述某业 述某业务 某业务系 业务系统 务系统上 系统上线 统上线多 上线多年 前端 端由 前端由 反向 向代 代理 反向代 向代理 反向代理 后端 端是 后端是
  16. 背景SSRF(Server-Side Request Forgery)在真实业务里并不少见,尤其是存在“远程图片抓取、Webhook 回调、URL 预览、文档转换、文件导入、第三方接口代理”等功能时,很容易把用户可控 URL 交给服务端发起请求。这类问题的麻烦点在于:表面上只是服务端访问一个 URL,但如果边界控制不足,可能导致内网探测、云元数据访问、访问本机管理端口、打到内部未鉴权接口等风险。本文不讨论攻击利用链,只整理一次代码审计中比较通用的识别、验证和修复方法,适合做安全评审或自查时参考。常见场景审计 SSRF 时,我通常会先按功能入口梳理,而不是一上来全局搜关键字。以下场景值得重点关注:用户头像、文章封面、商品图片的“远程 URL 上传”。富文本编辑器中的 URL 预览、链接解析、OpenGraph 抓取。Webhook 测试功能,允许用户配置回调地址。PDF/Word/HTML 转换服务,支持从 URL 拉取资源。接口代理、数据同步、第三方 API 转发。运维类后台中的健康检查、连通性测试、URL ping。这些功能共同特点是:用户提供一个地址,后端用 HTTP 客户端去访问。代码审计切入点从代码层面看,SSRF 的关键链路通常是:用户输入 URL → 参数校验 → DNS 解析 → 发起请求 → 跟随跳转 → 读取响应 → 保存或返回结果。审计时可以先搜索常见 HTTP 客户端调用点。以 Java 项目为例,常见关键字包括:RestTemplate WebClient HttpClient OkHttpClient URLConnection Jsoup.connect Request.Get CloseableHttpClientNode.js 项目里可以重点看:axios request got node-fetch http.request https.request undiciPHP 项目里则常见于:curl_exec file_get_contents fsockopen GuzzleHttp\\Client stream_context_create搜索到调用点后,不要只看这一行是否传入了 URL,更要看 URL 是否可控、是否存在重定向、是否做了 IP 段限制、是否允许非 HTTP 协议、是否存在二次请求。一个典型问题示例下面是审计中很常见的一类写法,业务含义是“根据 URL 下载远程图片”。代码本身不复杂,但安全边界较弱:public byte[] downloadImage(String url) throws IOException { URL target = new URL(url); HttpURLConnection conn = (HttpURLConnection) target.openConnection(); conn.setConnectTimeout(3000); conn.setReadTimeout(5000); conn.setInstanceFollowRedirects(true); try (InputStream in = conn.getInputStream()) { return in.readAllBytes(); } }这段代码的问题主要有几个:没有限制协议,理论上可能出现非预期协议处理。没有限制目标地址范围,内网地址、本机地址都可能被请求。允许自动跳转,首次 URL 可能是正常域名,但跳转目标可能发生变化。没有限制响应大小,可能导致内存压力。没有校验 Content-Type,非图片内容也会被读取。验证思路:以安全边界为主在授权测试或内部自查中,验证 SSRF 不建议直接访问敏感内部资源。更稳妥的方式是准备可控的测试端点,只验证“服务端是否会按用户输入发起请求”。例如在测试环境部署一个简单的 HTTP 服务,记录请求来源、路径、Header 和时间,用于判断服务端是否发起了请求:python3 -m http.server 8000如果需要观察更详细的请求,可以使用一个简单脚本:from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_GET(self): print(\"path:\", self.path) print(\"headers:\\n\", self.headers) self.send_response(200) self.send_header(\"Content-Type\", \"text/plain\") self.end_headers() self.wfile.write(b\"ok\") HTTPServer((\"0.0.0.0\", 8000), Handler).serve_forever()验证重点不是“能不能打到某个敏感地址”,而是确认以下行为:服务端是否会访问用户提交的 URL。请求是否发生在后端服务器,而不是浏览器端。是否跟随 301/302 跳转。是否会解析 DNS 并访问解析后的 IP。是否存在协议限制和地址限制。异常响应是否被原样返回给用户,造成信息泄露。重定向与 DNS 解析是高频坑点不少修复只在请求前做了一次字符串判断,例如禁止 URL 中包含 127.0.0.1、localhost、169.254 等关键字。这类方式很容易漏掉变体,也无法覆盖跳转和 DNS 解析变化。更合理的处理方式是:解析 URL → 校验协议 → 解析域名到 IP → 校验 IP 是否允许 → 发起请求 → 如果跳转,重新对 Location 目标执行同样校验。伪代码逻辑可以参考:1. 只允许 http 和 https 2. 解析 hostname 3. DNS 解析得到所有 IP 4. 任意一个 IP 落入禁止范围,则拒绝 5. 发起请求时禁用自动跳转 6. 如响应为 30x,取 Location 后重新走 1-5 7. 限制响应大小、超时时间、Content-Type禁止访问的地址范围至少应包含:127.0.0.0/8,本机回环地址。10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,常见私网地址。169.254.0.0/16,链路本地地址,云环境中尤其需要注意。0.0.0.0/8、224.0.0.0/4 等特殊用途地址。IPv6 的 ::1、fc00::/7、fe80::/10 等范围。修复示例:请求前后的双重限制下面给一个偏工程化的加固思路,不依赖单纯字符串过滤。具体实现可以结合项目语言和网络库调整。private static final Set&lt;String&gt; ALLOWED_SCHEMES = Set.of(\"http\", \"https\"); public boolean isAllowedUrl(String input) throws Exception { URI uri = new URI(input).normalize(); String scheme = uri.getScheme(); if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) { return false; } String host = uri.getHost(); if (host == null || host.isBlank()) { return false; } InetAddress[] addresses = InetAddress.getAllByName(host); for (InetAddress address : addresses) { if (isPrivateOrSpecialAddress(address)) { return false; } } return true; }地址判断不要只覆盖 IPv4,也要考虑 IPv6:private boolean isPrivateOrSpecialAddress(InetAddress address) { return address.isAnyLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress() || address.isMulticastAddress(); }这只是基础判断。生产环境里建议继续补充明确的 CIDR 判断,尤其是云厂商元数据地址、内部服务网段、容器网络网段等。不同 JDK 方法对某些地址分类并不完全等价,不能完全依赖一个内置方法解决所有场景。更推荐的策略:白名单优先如果业务允许,白名单比黑名单稳定得多。例如只允许访问固定的对象存储域名、公司 CDN 域名、合作方 API 域名。这样可以把 SSRF 的风险面缩小很多。一个相对稳妥的白名单策略:只允许固定域名或固定后缀域名。后缀匹配要避免 example.com.evil.test 这类误判。域名解析后仍要校验 IP,不要认为白名单域名永远安全。禁止用户自定义端口,或只允许 80、443。禁用自动跳转,跳转目标也必须重新校验。后缀匹配可以按边界判断,例如允许 example.com 和 *.example.com:boolean matchDomain(String host) { host = host.toLowerCase(Locale.ROOT); return host.equals(\"example.com\") || host.endsWith(\".example.com\"); }请求行为的安全限制即使 URL 校验已经做了,也建议在 HTTP 客户端层面加限制,减少异常情况带来的影响:连接超时和读取超时必须设置,避免线程被长期占用。限制最大响应体大小,例如图片抓取只允许几 MB。限制 Content-Type,例如只允许 image/jpeg、image/png、image/webp。限制请求方法,通常只需要 GET 或 HEAD。不要转发用户可控的敏感 Header。不要把后端请求错误细节完整回显给前端。例如下载远程图片时,应先读取响应头,再决定是否继续读取响应体:Content-Type: image/png Content-Length: 245760如果 Content-Length 缺失,也不能无限读取,应使用带上限的流读取方式。日志与监控SSRF 类问题在事故复盘时,日志经常是唯一线索。建议记录必要但不过度敏感的信息:发起请求的业务功能和用户标识。原始 URL、最终请求域名、解析到的 IP。响应状态码、耗时、响应大小。是否发生跳转及跳转目标。被策略拒绝的原因。同时可以对异常行为做告警,例如短时间内大量不同端口、不同内网段、异常协议、频繁超时等。告警规则不需要一开始很复杂,先覆盖明显异常就有价值。代码审计清单检查项建议URL 是否用户可控确认参数来源,注意间接传递和配置项覆盖协议限制只允许 http/https,拒绝其他协议域名校验优先白名单,避免简单 contains 判断IP 校验DNS 解析后校验所有返回 IP,覆盖 IPv4/IPv6重定向禁用自动跳转,跳转目标重新校验端口限制仅允许业务必要端口响应限制限制大小、类型、超时错误处理避免把内部错误、请求细节完整回显日志审计记录目标域名、解析 IP、状态码、拒绝原因几个容易忽略的细节只校验 URL 字符串不够,必须基于解析结果做判断。只校验首次请求不够,跳转后的目标也要校验。只考虑 IPv4 不够,IPv6 在不少环境中默认可用。只看业务代码不够,网关、代理、服务网格也可能改变请求路径。只做应用层限制不够,网络层出站访问控制同样重要。实际加固中,应用层校验和网络层隔离最好同时做。应用层负责拒绝明显不合理的输入,网络层负责兜底限制服务能访问的范围。总结SSRF 的本质不是“某个 HTTP 客户端有问题”,而是服务端替用户访问了不该访问的目标。审计时要围绕完整链路看:输入从哪里来、如何解析、访问到哪里、是否跳转、响应如何处理。修复上,不建议依赖关键字黑名单。更稳妥的方案是白名单域名、协议和端口限制、DNS 后 IP 校验、重定向重新校验、响应大小和类型限制,再配合网络层出站访问控制。这样即使某一层出现遗漏,也不至于直接暴露内部服务。 代码审计、调用链与关键函数定位示意 背景 在真 真实 实业 业务 务里 里并 并不 不少 少见 在真实 真实业 实业务 业务里 务里并 里并不 并不少 不少见 在真实业 真实业务 实业务里 业务里并 务里并不 里并不少 并不少见 尤其 其是 是存 存在 尤其是 其是存 是存在 尤其是存 其是存在 远程 程图 图片 片抓 抓取 远程图 程图片 图片抓 片抓取 远程图片 程图片抓 图片抓取 回调 预览 文档 档转 转换 文档转 档转换 文档转换 文件 件导 导入 文件导 件导入 文件导入 第三 三方 方接 接口 口代 代理 第三方 三方接 方接口 接口代 口代理 第三方接 三方接口 方接口代 接口代理 等功 功能 能时 等功能 功能时 等功能时 很容 容易 易把 把用 用户 户可 可控 很容易 容易把 易把用 把用户 用户可 户可控 很容易把 容易把用 易把用户 把用户可 用户可控 交给 给服 服务 务端 端发 发起 起请 请求 交给服 给服务 服务端 务端发 端发起 发起请 起请求 交给服务 给服务端 服务端发 务端发起 端发起请 发起请求 这类 类问 问题 题的 的麻 麻烦 烦点 点在 在于 这类问 类问题 问题的 题的麻 的麻烦 麻烦点 烦点在 点在于 这类问题 类问题的 问题的麻 题的麻烦 的麻烦点 麻烦点在 烦点在于 表面 面上 上只 只是 是服 端访 访问 问一 一个 表面上 面上只 上只是 只是服 是服务 务端访 端访问 访问一 问一个 表面上只 面上只是 上只是服 只是服务 是服务端 服务端访 务端访问 端访问一 访问一个 但如 如果 果边 边界 界控 控制 制不 不足 但如果 如果边 果边界 边界控 界控制 控制不 制不足 但如果边 如果边界 果边界控 边界控制 界控制不 控制不足 可能 能导 导致 致内 内网 网探 探测 可能导 能导致 导致内 致内网 内网探 网探测 可能导致 能导致内 导致内网 致内网探 内网探测 云元 元数 数据 据访 云元数 元数据 数据访 据访问 云元数据 元数据访 数据访问 问本 本机 机管 管理 理端 端口 访问本 问本机 本机管 机管理 管理端 理端口 访问本机 问本机管 本机管理 机管理端 管理端口 打到 到内 内部 部未 未鉴 鉴权 权接 口等 等风 风险 打到内 到内部 内部未 部未鉴 未鉴权 鉴权接 权接口 接口等 口等风 等风险 打到内部 到内部未 内部未鉴 部未鉴权 未鉴权接 鉴权接口 权接口等 接口等风 口等风险 本文 文不 不讨 讨论 论攻 攻击 击利 利用 用链 本文不 文不讨 不讨论 讨论攻 论攻击 攻击利 击利用 利用链 本文不讨 文不讨论 不讨论攻 讨论攻击 论攻击利 攻击利用 击利用链 只整 整理 理一 一次 次代 代码 码审 审计 计中 中比 比较 较通 通用 用的 的识 识别 只整理 整理一 理一次 一次代 次代码 代码审 码审计 审计中 计中比 中比较 比较通 较通用 通用的 用的识 的识别 只整理一 整理一次 理一次代 一次代码 次代码审 代码审计 码审计中 审计中比 计中比较 中比较通 比较通用 较通用的 通用的识 用的识别 验证 证和 和修 修复 复方 方法 验证和 证和修 和修复 修复方 复方法 验证和修 证和修复 和修复方 修复方法 适合 合做 做安 安全 全评 评审 审或 或自 自查 查时 时参 参考 适合做 合做安 做安全 安全评 全评审 评审或 审或自 或自查 自查时 查时参 时参考 适合做安 合做安全 做安全评 安全评审 全评审或 评审或自 审或自查 或自查时 自查时参 查时参考 常见 见场 场景 景审 常见场 见场景 场景审 景审计 常见场景 见场景审 场景审计 我通 通常 常会 会先 先按 按功 能入 入口 口梳 梳理 我通常 通常会 常会先 会先按 先按功 按功能 功能入 能入口 入口梳 口梳理 我通常会 通常会先 常会先按 会先按功 先按功能 按功能入 功能入口 能入口梳 入口梳理 而不 不是 是一 一上 上来 来全 全局 局搜 搜关 关键 键字 而不是 不是一 是一上 一上来 上来全 来全局 全局搜 局搜关 搜关键 关键字 而不是一 不是一上 是一上来 一上来全 上来全局 来全局搜 全局搜关 局搜关键 搜关键字 以下 下场 景值 值得 得重 重点 点关 关注 以下场 下场景 场景值 景值得 值得重 得重点 重点关 点关注 以下场景 下场景值 场景值得 景值得重 值得重点 得重点关 重点关注 户头 头像 用户头 户头像 用户头像 文章 章封 封面 文章封 章封面 文章封面 商品 品图 片的 商品图 品图片 图片的 商品图片 品图片的 上传 富文 文本 本编
  17. 背景任意文件读取是 Web 应用里比较常见、也容易被低估的一类漏洞。它不像远程命令执行那样直接,但在真实事件里经常成为突破口:读取配置文件、源码、密钥、日志、备份文件后,攻击面会迅速扩大。 这篇文章整理一次 Java Web 项目代码审计中的典型问题。场景做了脱敏和简化,重点放在漏洞成因、审计方法、复现边界和修复思路上,避免涉及对真实系统的攻击利用。 问题场景项目中有一个文件预览接口,用于给后台用户查看上传后的附件。接口大致逻辑是:前端传入文件名,后端拼接上传目录后读取文件内容并返回。 GET /admin/file/preview?name=report.pdf从业务上看,这类接口很常见,尤其是后台管理系统、工单系统、知识库系统中。但如果文件名参数没有做严格约束,就容易出现路径穿越,最终导致任意文件读取。 问题代码示例审计时看到的代码结构大致如下: String baseDir = uploadConfig.getBaseDir(); String name = request.getParameter("name"); File file = new File(baseDir + File.separator + name); if (!file.exists()) { response.setStatus(404); return; } try (InputStream in = new FileInputStream(file)) { IOUtils.copy(in, response.getOutputStream()); }这段代码的主要问题不在于使用了 FileInputStream,而在于直接信任了用户输入的 name 参数。只要参数中包含路径跳转符号,就可能脱离预期的上传目录。 技术分析路径穿越漏洞的核心点是:应用以为自己访问的是某个受控目录下的文件,但操作系统在解析路径后,实际访问的可能是受控目录之外的文件。 例如,应用上传目录为: /data/app/uploads如果代码直接拼接用户输入: /data/app/uploads/../config/application.yml操作系统规范化路径后,实际访问路径可能变成: /data/app/config/application.yml如果缺少边界校验,应用层很难意识到访问已经越界。 审计关注点做代码审计时,我一般会优先检索以下几类代码: new File( FileInputStream Files.readAllBytes Files.copy Paths.get ResourceUtils.getFile response.getOutputStream之后重点看这些 API 的路径参数来源。如果路径参数来自以下位置,就需要进一步确认是否做了安全处理: HTTP 请求参数,如 name、path、file、url。请求头,如导出、下载、预览相关的自定义头。数据库字段,尤其是用户可提交、可编辑的附件路径。消息队列或异步任务参数。压缩包解压后的文件名。仅检查是否包含 ../ 并不可靠。不同系统、不同编码方式、不同路径分隔符都会带来绕过风险。更稳妥的方式是做路径规范化后再判断边界。 安全复现边界在测试环境验证时,不建议去读取系统敏感文件。更合适的做法是在受控环境里放置一个标记文件,用于判断是否能越过上传目录。 例如测试目录结构如下: /tmp/demo/ ├── uploads/ │ └── normal.txt └── marker.txt接口预期只能读取 /tmp/demo/uploads 下的文件。如果通过参数能读取到 /tmp/demo/marker.txt,就可以证明存在越权读取风险。 GET /admin/file/preview?name=../marker.txt这里的验证方式只针对本地或授权测试环境,不涉及真实业务系统的敏感文件,也便于后续写入测试报告和修复验证。 常见错误修复方式这类漏洞的修复经常会出现一些看似有效、实际不稳的方案。 只用字符串判断 contains("../"),容易被编码、不同分隔符、重复规范化绕过。只限制文件后缀,如只允许 .pdf,但路径仍可能越界。只判断文件是否存在,不判断是否位于允许目录内。将用户传入路径直接存入数据库,后续读取时默认可信。使用黑名单过滤敏感目录,维护成本高且容易遗漏。推荐修复思路比较可靠的修复方式是:固定基准目录,对用户输入做白名单约束,并在路径规范化后判断是否仍在基准目录内。 Path basePath = Paths.get(uploadConfig.getBaseDir()).toRealPath(); String name = request.getParameter("name"); if (name == null || !name.matches("^[a-zA-Z0-9._-]{1,100}$")) { response.setStatus(400); return; } Path targetPath = basePath.resolve(name).normalize(); if (!targetPath.startsWith(basePath)) { response.setStatus(403); return; } if (!Files.isRegularFile(targetPath)) { response.setStatus(404); return; } try (InputStream in = Files.newInputStream(targetPath)) { in.transferTo(response.getOutputStream()); }上面示例里有几个关键点: basePath 使用 toRealPath() 获取真实路径,减少符号链接等因素带来的误判。文件名使用白名单,只允许简单文件名,而不是任意路径。resolve 后调用 normalize,再用 startsWith 判断是否仍在基准目录下。使用 Files.isRegularFile,避免读取目录、设备文件等非普通文件。更稳的业务设计如果业务允许,建议不要让前端直接传文件路径或文件名,而是传文件 ID。后端根据 ID 查询数据库中的附件记录,再拼接服务端保存的相对文件名。 GET /admin/file/preview?id=1024服务端流程可以调整为: 根据当前用户和文件 ID 查询附件记录。确认该用户是否有权限访问该附件。从数据库中取服务端生成的存储名,而不是使用用户提交的路径。按固定目录读取文件,并进行路径边界检查。这种方式能同时解决两个问题:路径穿越和水平越权。很多文件读取接口表面是路径问题,深入看其实还伴随权限边界不清。 日志与检测建议修复之外,建议对异常路径访问做基础审计。比如出现以下特征时记录安全日志: 参数中包含 ..、/、\ 等路径符号。出现 URL 编码后的路径跳转字符。访问不存在文件的频率明显异常。同一账号短时间内访问大量不同文件名。日志不要记录完整敏感路径和文件内容,重点记录用户、接口、参数摘要、客户端地址、拒绝原因即可。 log.warn("Blocked suspicious file access, userId={}, api={}, reason={}, nameHash={}", userId, "/admin/file/preview", "path_boundary_check_failed", sha256(name));代码审计清单检查项说明路径参数来源确认文件路径是否来自用户输入、数据库或外部系统是否使用白名单优先允许简单文件名或文件 ID,避免任意路径是否规范化路径使用 normalize、toRealPath 等方式处理路径是否校验目录边界读取前判断目标路径是否仍在允许目录内是否校验权限确认当前用户是否有权访问目标文件是否限制文件类型结合业务限制后缀、MIME、大小等属性是否记录异常访问对疑似探测行为进行安全审计注意点Windows 和 Linux 的路径分隔符不同,审计时不要只考虑一种系统。压缩包解压也会遇到类似问题,俗称 Zip Slip,需要对压缩包内文件名做同样的边界检查。如果上传目录中允许符号链接,需要特别谨慎,避免链接指向目录外。下载、预览、导出、模板读取、日志查看接口都属于高频风险点。修复后要补充单元测试,覆盖正常文件名、路径跳转、空值、超长文件名、特殊字符等情况。总结任意文件读取漏洞的根因通常不是某个 API 本身危险,而是路径边界没有定义清楚。只要接口允许用户影响文件路径,就应该默认存在风险,并通过白名单、路径规范化、目录边界校验和权限控制来收敛。 在代码审计中,这类问题值得长期关注。它复现成本低、影响面容易扩大,且经常隐藏在后台功能、运维功能和文件预览功能里。修复时不要停留在过滤字符串,应该从业务设计和路径模型上把边界收紧。 代码审计、调用链与关键函数定位示意 背景 景任 任意 意文 文件 件读 读取 取是 背景任 景任意 任意文 意文件 文件读 件读取 读取是 背景任意 景任意文 任意文件 意文件读 文件读取 件读取是 应用 用里 里比 比较 较常 常见 应用里 用里比 里比较 比较常 较常见 应用里比 用里比较 里比较常 比较常见 也容 容易 易被 被低 低估 估的 的一 一类 类漏 漏洞 也容易 容易被 易被低 被低估 低估的 估的一 的一类 一类漏 类漏洞 也容易被 容易被低 易被低估 被低估的 低估的一 估的一类 的一类漏 一类漏洞 它不 不像 像远 远程 程命 命令 令执 执行 行那 那样 样直 直接 它不像 不像远 像远程 远程命 程命令 命令执 令执行 执行那 行那样 那样直 样直接 它不像远 不像远程 像远程命 远程命令 程命令执 命令执行 令执行那 执行那样 行那样直 那样直接 但在 在真 真实 实事 事件 件里 里经 经常 常成 成为 为突 突破 破口 但在真 在真实 真实事 实事件 事件里 件里经 里经常 经常成 常成为 成为突 为突破 突破口 但在真实 在真实事 真实事件 实事件里 事件里经 件里经常 里经常成 经常成为 常成为突 成为突破 为突破口 取配 配置 置文 读取配 取配置 配置文 置文件 读取配置 取配置文 配置文件 源码 密钥 日志 备份 份文 件后 备份文 份文件 文件后 备份文件 份文件后 攻击 击面 面会 会迅 迅速 速扩 扩大 攻击面 击面会 面会迅 会迅速 迅速扩 速扩大 攻击面会 击面会迅 面会迅速 会迅速扩 迅速扩大 这篇 篇文 文章 章整 整理 理一 一次 这篇文 篇文章 文章整 章整理 整理一 理一次 这篇文章 篇文章整 文章整理 章整理一 整理一次 项目 目代 代码 码审 审计 计中 中的 的典 典型 型问 问题 项目代 目代码 代码审 码审计 审计中 计中的 中的典 的典型 典型问 型问题 项目代码 目代码审 代码审计 码审计中 审计中的 计中的典 中的典型 的典型问 典型问题 场景 景做 做了 了脱 脱敏 敏和 和简 简化 场景做 景做了 做了脱 了脱敏 脱敏和 敏和简 和简化 场景做了 景做了脱 做了脱敏 了脱敏和 脱敏和简 敏和简化 重点 点放 放在 在漏 洞成 成因 重点放 点放在 放在漏 在漏洞 漏洞成 洞成因 重点放在 点放在漏 放在漏洞 在漏洞成 漏洞成因 计方 方法 审计方 计方法 审计方法 复现 现边 边界 界和 和修 修复 复思 思路 路上 复现边 现边界 边界和 界和修 和修复 修复思 复思路 思路上 复现边界 现边界和 边界和修 界和修复 和修复思 修复思路 复思路上 避免 免涉 涉及 及对 对真 实系 系统 统的 的攻 击利 利用 避免涉 免涉及 涉及对 及对真 对真实 真实系 实系统 系统的 统的攻 的攻击 攻击利 击利用 避免涉及 免涉及对 涉及对真 及对真实 对真实系 真实系统 实系统的 系统的攻 统的攻击 的攻击利 攻击利用 题场 景项 目中 中有 有一 一个 个文 件预 预览 览接 接口 问题场 题场景 场景项 景项目 项目中 目中有 中有一 有一个 一个文 个文件 文件预 件预览 预览接 览接口 问题场景 题场景项 场景项目 景项目中 项目中有 目中有一 中有一个 有一个文 一个文件 个文件预 文件预览 件预览接 预览接口 用于 于给 给后 后台 台用 用户 户查 查看 看上 上传 传后 后的 的附 附件 用于给 于给后 给后台 后台用 台用户 用户查 户查看 查看上 看上传 上传后 传后的 后的附 的附件 用于给后 于给后台 给后台用 后台用户 台用户查 用户查看 户查看上 查看上传 看上传后 上传后的 传后的附 后的附件 口大 大致 致逻 逻辑 辑是 接口大 口大致 大致逻 致逻辑 逻辑是 接口大致 口大致逻 大致逻辑 致逻辑是 前端 端传 传入 入文 件名 前端传 端传入 传入文 入文件 文件名 前端传入 端传入文 传入文件 入文件名 后端 端拼 拼接 接上 传目 目录 录后 后读 取文 件内 内容 容并 并返 返回 后端拼 端拼接 拼接上 接上传 上传目 传目录 目录后 录后读 后读取 读取文 取文件 文件内 件内容 内容并 容并返 并返回 后端拼接 端拼接上 拼接上传 接上传目 上传目录 传目录后 目录后读 录后读取 后读取文 读取文件 取文件内 文件内容 件内容并 内容并返 容并返回 从业 业务 务上 上看 从业务 业务上 务上看 从业务上 业务上看 这类 类接 口很 很常 这类接 类接口 接口很 口很常 很常见 这类接口 类接口很 接口很常 口很常见 尤其 其是 是后 台管 管理 理系 尤其是 其是后
  18. 背景文件上传是 Web 系统里最常见、也最容易被低估的功能。很多业务团队会把它理解成“把文件放到对象存储或服务器目录”,但从安全视角看,上传接口同时涉及鉴权、文件类型识别、存储隔离、访问控制、内容解析、异步处理等多个环节。任何一个环节处理不严,都可能从普通的信息泄露演变成更严重的服务端风险。 这篇文章整理一次常见上传接口的代码审计思路,重点放在如何识别风险、如何验证问题、以及如何收敛风险。内容只讨论防护和审计方法,不涉及攻击利用链扩展。 问题场景某业务系统提供头像、附件、报表模板等上传能力,后端接口大致逻辑如下: 前端限制文件后缀,例如只允许 jpg、png、pdf、xlsx。后端接收 MultipartFile 后,根据原始文件名取后缀。将文件保存到 Web 目录或挂载目录。返回可访问 URL 给前端。从代码审计角度,这类实现至少需要关注几个问题: 是否只依赖前端校验。后端是否使用白名单校验,而不是黑名单拦截。是否信任 Content-Type。是否直接使用用户提交的文件名。上传目录是否可执行、可解析动态脚本。返回 URL 是否导致未授权访问或敏感文件暴露。图片、Office、压缩包等文件是否进入了解析流程。典型代码风险点下面是一段简化后的 Java 上传代码,问题在实际项目里很常见: String originalName = file.getOriginalFilename(); String suffix = originalName.substring(originalName.lastIndexOf(".") + 1); if (!Arrays.asList("jpg", "png", "pdf").contains(suffix)) { throw new RuntimeException("file type not allowed"); } String savePath = uploadDir + "/" + originalName; file.transferTo(new File(savePath)); return "/uploads/" + originalName;这段代码的问题不在于“能不能跑”,而在于安全边界太弱: 后缀未统一小写,可能出现大小写绕过导致策略失效。直接使用原始文件名,可能带来路径穿越、覆盖已有文件、特殊字符处理异常等问题。只看后缀,不校验文件真实类型。上传目录如果位于 Web 根目录,容易引入直接访问风险。返回固定路径,缺少权限控制和访问时效控制。审计时的关键检查点1. 鉴权与业务边界先看接口是否需要登录、是否校验用户身份、是否限制上传用途。很多系统只有“上传接口”,没有区分头像上传、合同上传、模板上传等业务场景,最终所有文件进入同一个目录,这会放大风险。 建议至少检查: 接口是否存在统一鉴权。是否校验用户对业务对象的操作权限。是否限制单用户上传频率和文件总量。是否区分公开文件和私有文件。例如,附件上传不应只判断用户是否登录,还应判断用户是否有权给当前订单、工单或项目添加附件。 2. 文件名处理不要使用用户提交的原始文件名作为落盘文件名。原始文件名只适合做展示字段,保存时应生成不可预测的新文件名,并将展示名单独存储。 String originalName = StringUtils.cleanPath(file.getOriginalFilename()); String ext = getSafeExtension(originalName); String objectName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path target = uploadBase.resolve(objectName).normalize(); if (!target.startsWith(uploadBase)) { throw new SecurityException("invalid upload path"); } Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);这里需要注意,normalize 和 startsWith 必须配合使用,避免路径拼接后跳出预期目录。 3. 类型识别不能只靠后缀后缀、Content-Type、文件头魔数都不是单独可靠的判断依据。比较稳妥的方式是多条件组合:业务白名单后缀 + 服务端 MIME 识别 + 文件头检查 + 必要时解码验证。 private static final Set<String> ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "pdf"); private static final Set<String> ALLOWED_MIME = Set.of("image/jpeg", "image/png", "application/pdf"); String ext = getSafeExtension(originalName).toLowerCase(Locale.ROOT); if (!ALLOWED_EXT.contains(ext)) { throw new IllegalArgumentException("extension not allowed"); } String detected = tika.detect(file.getInputStream()); if (!ALLOWED_MIME.contains(detected)) { throw new IllegalArgumentException("mime not allowed"); }图片类文件还可以进一步尝试使用标准库解码,确认确实是可解析图片。需要注意的是,解析本身也可能引入风险,因此依赖库要保持更新,解析过程最好放在隔离环境或异步任务中。 4. 存储目录与执行权限上传文件不建议放在应用 Web 根目录下,更不应放在会被脚本引擎解析的目录。比较合理的方式是: 文件存储在 Web 根目录之外。通过受控下载接口读取文件。静态访问使用对象存储,并配置只读、禁止脚本执行。上传目录与应用代码目录分离。Nginx 场景下,如果确实需要提供静态访问,建议显式关闭可能的脚本解析,并限制自动索引: location /uploads/ { alias /data/app/uploads/; autoindex off; default_type application/octet-stream; location ~* \.(php|jsp|jspx|asp|aspx)$ { return 403; } }如果后端是 Tomcat、Jetty 等 Java 容器,也要避免把上传目录映射到可执行上下文里。不要认为“我们是 Java 项目,所以上传脚本没影响”,不少风险来自配置错误、网关映射、历史目录遗留和中间件特性。 5. 下载访问控制上传后的文件访问同样重要。很多系统上传时校验了用户,下载时却只靠一个文件 URL,最终导致越权访问。 更稳妥的设计是让下载请求经过业务鉴权: GET /api/files/{fileId}/download后端根据 fileId 查询文件归属、业务对象和当前用户权限,再决定是否返回内容。不要直接暴露真实磁盘路径,也不要让用户可控参数参与任意文件读取。 如果使用对象存储,可以返回短时效签名 URL,但签名生成前仍然要做业务权限判断。 6. 文件大小、数量与资源消耗上传接口也经常成为资源消耗点。审计时不要只看类型校验,还要检查限制是否完整: 单文件大小限制。单次请求总大小限制。单用户、单业务对象的数量限制。上传频率限制。异常中断后的临时文件清理。Spring Boot 中可配置基础大小限制: spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=20MB但这只是框架层限制,业务层仍需根据场景做更细的控制。例如头像和合同附件的大小限制不应该一样。 7. 压缩包与复杂格式处理如果系统支持 zip、tar、docx、xlsx 等复杂格式,需要额外谨慎。常见风险包括: 压缩包解压路径穿越。解压后文件数量过多。压缩比异常导致资源耗尽。Office、PDF 解析库漏洞。导入模板中的公式、外部链接、宏等内容。解压时必须对每个条目的路径做 normalize 检查: Path destDir = Paths.get("/data/import/tmp").toAbsolutePath().normalize(); Path target = destDir.resolve(entry.getName()).normalize(); if (!target.startsWith(destDir)) { throw new SecurityException("invalid zip entry path"); }同时限制条目数量、总解压大小和最大嵌套层级。不要在主业务进程里直接处理不可信复杂文件,必要时用独立 worker、容器或低权限用户处理。 推荐的安全实现流程一个相对稳妥的上传流程可以拆成以下步骤: 接口鉴权,确认当前用户可以执行该业务上传。校验文件大小、数量和业务类型。提取并规范化原始文件名,仅用于展示。使用白名单校验扩展名。服务端检测 MIME 和文件头。生成随机存储名,避免覆盖和枚举。保存到非 Web 根目录或对象存储隔离桶。记录文件元数据,包括归属用户、业务对象、hash、大小、类型。访问时通过 fileId 做权限判断,不直接暴露真实路径。对需要解析的文件进入隔离队列,并做好超时和资源限制。审计清单检查项风险表现建议鉴权未登录或低权限用户可上传统一认证并绑定业务权限文件名路径穿越、覆盖文件、特殊字符异常生成随机文件名,原名仅展示类型校验只看后缀或 Content-Type后缀、MIME、文件头组合校验存储位置上传目录可被直接解析放 Web 根目录外,关闭执行权限访问控制知道 URL 即可下载通过下载接口做权限判断资源限制大文件或高频上传拖垮服务限制大小、频率、数量和总量复杂文件解析解析库漏洞或资源耗尽隔离处理,限制超时和资源注意点不要把前端限制当作安全控制,前端校验只能改善体验。不要使用黑名单方式拦截危险后缀,实际维护成本高且容易遗漏。不要把上传目录和应用代码目录混放。不要在日志中记录完整敏感文件内容或可长期访问的私有文件 URL。不要忽略错误处理,上传失败后的临时文件和数据库记录需要一致性清理。不要默认对象存储桶公开可读,公开文件和私有文件应分桶或分前缀隔离。总结文件上传接口的安全性不取决于某一个校验点,而取决于整条链路是否有边界。审计时建议从“谁能上传、能上传什么、存到哪里、谁能访问、是否会被解析、异常如何处理”这几个问题入手。 实际整改中,不一定要一次性改完整个文件系统,但至少应优先处理三类高风险点:上传目录可执行、下载缺少权限控制、复杂文件直接在主进程解析。把这些问题收敛后,再逐步补齐类型识别、资源限制、审计日志和隔离处理,整体风险会下降很多。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 系统 统里 里最 最常 常见 系统里 统里最 里最常 最常见 系统里最 统里最常 里最常见 也最 最容 容易 易被 被低 低估 估的 的功 功能 也最容 最容易 容易被 易被低 被低估 低估的 估的功 的功能 也最容易 最容易被 容易被低 易被低估 被低估的 低估的功 估的功能 很多 多业 业务 务团 团队 队会 会把 把它 它理 理解 解成 很多业 多业务 业务团 务团队 团队会 队会把 会把它 把它理 它理解 理解成 很多业务 多业务团 业务团队 务团队会 团队会把 队会把它 会把它理 把它理解 它理解成 把文 件放 放到 到对 对象 象存 存储 储或 或服 服务 务器 器目 目录 把文件 文件放 件放到 放到对 到对象 对象存 象存储 存储或 储或服 或服务 服务器 务器目 器目录 把文件放 文件放到 件放到对 放到对象 到对象存 对象存储 象存储或 存储或服 储或服务 或服务器 服务器目 务器目录 但从 从安 安全 全视 视角 角看 但从安 从安全 安全视 全视角 视角看 但从安全 从安全视 安全视角 全视角看 传接 接口 口同 同时 时涉 涉及 及鉴 鉴权 上传接 传接口 接口同 口同时 同时涉 时涉及 涉及鉴 及鉴权 上传接口 传接口同 接口同时 口同时涉 同时涉及 时涉及鉴 涉及鉴权 件类 类型 型识 识别 文件类 件类型 类型识 型识别 文件类型 件类型识 类型识别 储隔 隔离 存储隔 储隔离 存储隔离 访问 问控 控制 访问控 问控制 访问控制 内容 容解 解析 内容解 容解析 内容解析 异步 步处 处理 理等 等多 多个 个环 环节 异步处 步处理 处理等 理等多 等多个 多个环 个环节 异步处理 步处理等 处理等多 理等多个 等多个环 多个环节 任何 何一 一个 节处 理不 不严 任何一 何一个 一个环 环节处 节处理 处理不 理不严 任何一个 何一个环 一个环节 个环节处 环节处理 节处理不 处理不严 都可 可能 能从 从普 普通 通的 的信 信息 息泄 泄露 露演 演变 变成 成更 更严 严重 重的 的服 务端 端风 风险 都可能 可能从 能从普 从普通 普通的 通的信 的信息 信息泄 息泄露 泄露演 露演变 演变成 变成更 成更严 更严重 严重的 重的服 的服务 服务端 务端风 端风险 都可能从 可能从普 能从普通 从普通的 普通的信 通的信息 的信息泄 信息泄露 息泄露演 泄露演变 露演变成 演变成更 变成更严 成更严重 更严重的 严重的服 重的服务 的服务端 服务端风 务端风险 这篇 篇文 文章 章整 整理 理一 一次 次常 见上 口的 的代 代码 码审 审计 计思 思路 这篇文 篇文章 文章整 章整理 整理一 理一次 一次常 次常见 常见上 见上传 接口的 口的代 的代码 代码审 码审计 审计思 计思路 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次常 一次常见 次常见上 常见上传 见上传接 传接口的 接口的代 口的代码 的代码审 代码审计 码审计思 审计思路 重点 点放 放在 在如 如何 何识 别风 重点放 点放在 放在如 在如何 如何识 何识别 识别风 别风险 重点放在 点放在如 放在如何 在如何识 如何识别 何识别风 识别风险 何验 验证 证问 问题 如何验 何验证 验证问 证问题 如何验证 何验证问 验证问题 以及 及如 何收 收敛 敛风 以及如 及如何 如何收 何收敛 收敛风 敛风险 以及如何 及如何收 如何收敛 何收敛风 收敛风险 容只 只讨 讨论 论防 防护 护和 和审 计方 方法 内容只 容只讨 只讨论 讨论防 论防护 防护和 护和审 和审计 审计方 计方法 内容只讨 容只讨论 只讨论防 讨论防护 论防护和 防护和审 护和审计 和审计方 审计方法 不涉 及攻 攻击 击利 利用 用链 链扩 扩展 不涉及 涉及攻 及攻击 攻击利 击利用 利用链 用链扩 链扩展 不涉及攻 涉及攻击 及攻击利 攻击利用 击利用链 利用链扩 用链扩展 题场 场景 景某 某业 务系 统提 提供 供头 头像 问题场 题场景 场景某 景某业 某业务 业务系 务系统 系统提 统提供 提供头 供头像 问题场景 题场景某 场景某业 景某业务 某业务系 业务系统 务系统提 系统提供 统提供头 提供头像 附件 报表 表模 模板 板等 等上 传能 能力 报表模 表模板 模板等 板等上 等上传 上传能 传能力 报表模板 表模板等 模板等上 板等上传 等上传能 上传能力 后端 端接 口大 大致 致逻 逻辑 辑如 如下 后端接 端接口 接口大
  19. SSRF(Server-Side Request Forgery,服务端请求伪造)并不只是一个“能让服务器发起请求”的问题。真正影响风险的,通常是请求目标是否可控、服务器所在网络能访问哪些资源、应用是否跟随重定向,以及响应内容是否会回显给用户。本文以代码审计和本地实验为主线,整理一套适合企业 Web 应用的 SSRF 识别、验证与修复方法。一、问题背景与常见场景SSRF 常出现在需要由后端访问外部地址的功能中,例如图片远程导入、Webhook、在线截图、文档转换、URL 预览、第三方接口代理和云资源同步。典型代码逻辑是:用户提交一个 URL,服务端使用 HTTP 客户端访问该地址,再将状态码、响应内容或处理结果返回给用户。风险通常来自以下几个方面:攻击者可以控制完整 URL,服务端缺少协议、域名和端口限制。应用所在网络能够访问仅对内开放的管理接口、数据库面板或内部服务。服务端将响应正文直接返回,导致内部信息泄露。应用允许自动跟随重定向,初始校验通过后又访问了不可信目标。仅使用字符串黑名单拦截内网地址,容易被不同格式的 IP 表示绕过。二、代码审计时的定位思路审计时不要只搜索 SSRF 关键词,更有效的方法是从“用户输入如何进入网络请求”这条数据流入手。可以重点关注以下调用点:Java 中的 URL、HttpURLConnection、Apache HttpClient、OkHttp。Python 中的 requests、urllib、aiohttp。Node.js 中的 axios、fetch、http.request。Go 中的 net/http.Client、http.Get 和自定义 Transport。图像、PDF、网页截图等组件中隐式发起网络请求的功能。审计时建议记录四个关键问题:目标地址来自哪里;是否限制了协议;域名解析后的 IP 是否经过校验;重定向、代理和 DNS 解析是否仍在安全边界内。只看到一个 HTTP 请求调用,并不能直接判断漏洞是否成立,必须沿着完整数据流分析校验逻辑。三、容易被忽略的校验细节很多修复方案只判断 URL 字符串是否包含 127.0.0.1、localhost 或某些内网网段,这种方式可靠性较低。实际校验应当基于解析后的主机地址,并同时考虑 IPv4、IPv6、IPv4-mapped IPv6、十进制或十六进制 IP 表示,以及域名解析结果。此外,DNS 解析和真正建立连接之间可能存在时间窗口。如果校验阶段解析到公网地址,而建立连接阶段重新解析后得到内网地址,就可能形成 DNS rebinding 风险。因此,生产环境不应只依赖应用层字符串判断,还应结合出口代理、防火墙和网络分段控制。重定向也是常见遗漏点。第一跳地址可能是允许访问的公网域名,但服务端自动跟随 301 或 302 后访问了受限网段。对于需要严格控制目标的场景,建议默认关闭自动重定向;如果业务必须支持,应对每一跳重新执行协议、端口、域名和 IP 校验。四、在本地环境进行安全验证验证时应使用自建的本地测试服务,不要探测不属于自己的内部系统。可以启动一个简单的监听服务,用于确认请求是否由目标应用发出:python3 -m http.server 18080 --bind 127.0.0.1然后在测试应用中提交仅指向本机实验服务的 URL,例如: 127.0.0.1、IPv6 回环地址和内网保留地址时是否被阻断。重定向到受限地址时是否会被再次校验。如果应用没有响应正文回显,也不能直接认为不存在 SSRF。盲 SSRF 仍可能通过 DNS 日志、测试服务访问日志或应用自身的请求审计日志确认。验证过程中应只使用无敏感内容的自建服务,并保留时间、请求路径和应用日志,方便复盘。五、一个相对稳妥的防护实现防护应当采用白名单思路,而不是持续补充黑名单。理想情况下,业务只允许访问明确登记的域名、协议和端口,并通过统一出口代理转发。下面给出一个简化的 Python 校验示例,重点展示校验顺序,生产环境还应结合网络策略和连接层控制:from ipaddress import ip_address, ip_network from urllib.parse import urlparse import socket PRIVATE_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_private_or_reserved(value): address = ip_address(value) return any(address in network for network in PRIVATE_NETS) def validate_target(raw_url): parsed = urlparse(raw_url) if parsed.scheme not in ('http', 'https'): raise ValueError('unsupported scheme') if parsed.username or parsed.password: raise ValueError('userinfo is not allowed') if not parsed.hostname: raise ValueError('missing hostname') port = parsed.port or (443 if parsed.scheme == 'https' else 80) if port not in (80, 443): raise ValueError('unsupported port') addresses = socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM) for item in addresses: ip_value = item[4][0] if is_private_or_reserved(ip_value): raise ValueError('target address is not allowed') return parsed这段代码只能作为审计和设计参考,不能单独当作完整防线。实际实现中还需要避免解析后重新按域名连接,尽量将经过校验的地址绑定到连接过程;对于代理架构,要明确代理本身是否会再次解析域名;同时限制响应大小、连接超时、读取超时和最大重定向次数。六、工程化修复建议协议采用白名单,通常只允许 http 和 https,禁止 file、gopher、ftp 等非业务协议。域名采用业务白名单,必要时只允许固定后缀并进行精确匹配,避免 example.com.evil.test 之类的误判。限制端口范围,避免让通用 HTTP 客户端访问任意服务端口。在解析后检查所有 IP 地址,覆盖 IPv4、IPv6、回环、链路本地、组播、保留和私有网段。默认关闭自动重定向,业务需要时对每一跳重新校验。设置连接超时、读取超时、响应体大小和并发限制,降低资源消耗风险。通过网络出口代理统一控制外连,并在防火墙层禁止应用服务器直连敏感管理网段。记录目标域名、解析结果、最终连接地址、状态码、耗时和拦截原因,但不要把完整响应正文写入普通业务日志。七、常见错误修复方式第一种错误是只在前端限制 URL。前端校验可以改善交互体验,但不能替代服务端校验,任何请求都可能绕过浏览器直接提交。第二种错误是仅判断字符串中的 localhost 或 127.0.0.1。应当对 URL 进行规范解析,并对解析结果进行 IP 层判断。第三种错误是先校验域名,之后让底层 HTTP 库自由处理重定向和 DNS。安全边界必须覆盖整个请求生命周期,而不是只覆盖第一跳。第四种错误是修复后没有回归测试。建议将协议、端口、内网地址、IPv6 地址、域名解析异常、重定向和超时等场景加入自动化测试,避免后续更换 HTTP 客户端或代理配置时重新引入问题。八、总结SSRF 的核心不是某个特殊字符串,而是“不受信任的目标地址进入了服务端网络请求能力”。可靠的分析方法是沿数据流定位请求点,确认协议、主机、端口、解析、重定向和响应处理,再结合本地服务完成最小化验证。可靠的修复则应以白名单、IP 层校验、重定向控制、出口网络隔离和持续回归测试为基础。对于安全团队,建议把 SSRF 检查纳入代码审计清单和上线前测试;对于开发团队,最好将安全请求封装成统一组件,避免每个业务模块自行实现 URL 校验。这样不仅能减少单点遗漏,也便于后续集中更新策略和审计日志。 代码审计、调用链与关键函数定位示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不只 只是 是一 一个 并不只 不只是 只是一 是一个 并不只是 不只是一 只是一个 能让 让服 务器 器发 发起 起请 能让服 让服务 服务器 务器发 器发起 发起请 起请求 能让服务 让服务器 服务器发 务器发起 器发起请 发起请求 的问 问题 的问题 真正 正影 影响 响风 风险 险的 真正影 正影响 影响风 响风险 风险的 真正影响 正影响风 影响风险 响风险的 通常 常是 是请 求目 目标 标是 是否 否可 可控 通常是 常是请 是请求 请求目 求目标 目标是 标是否 是否可 否可控 通常是请 常是请求 是请求目 请求目标 求目标是 目标是否 标是否可 是否可控 器所 所在 在网 网络 络能 能访 访问 问哪 哪些 些资 资源 务器所 器所在 所在网 在网络 网络能 络能访 能访问 访问哪 问哪些 哪些资 些资源 服务器所 务器所在 器所在网 所在网络 在网络能 网络能访 络能访问 能访问哪 访问哪些 问哪些资 哪些资源 应用 用是 否跟 跟随 随重 重定 定向 应用是 用是否 是否跟 否跟随 跟随重 随重定 重定向 应用是否 用是否跟 是否跟随 否跟随重 跟随重定 随重定向 以及 及响 响应 应内 内容 容是 否会 会回 回显 显给 给用 用户 以及响 及响应 响应内 应内容 内容是 容是否 是否会 否会回 会回显 回显给 显给用 给用户 以及响应 及响应内 响应内容 应内容是 内容是否 容是否会 是否会回 否会回显 会回显给 回显给用 显给用户 本文 文以 以代 代码 码审 审计 计和 和本 本地 地实 实验 验为 为主 主线 本文以 文以代 以代码 代码审 码审计 审计和 计和本 和本地 本地实 地实验 实验为 验为主 为主线 本文以代 文以代码 以代码审 代码审计 码审计和 审计和本 计和本地 和本地实 本地实验 地实验为 实验为主 验为主线 整理 理一 一套 套适 适合 合企 企业 整理一 理一套 一套适 套适合 适合企 合企业 整理一套 理一套适 一套适合 套适合企 适合企业 用的 应用的 识别 验证 证与 与修 修复 复方 方法 验证与 证与修 与修复 修复方 复方法 验证与修 证与修复 与修复方 修复方法 题背 背景 景与 与常 常见 见场 场景 问题背 题背景 背景与 景与常 与常见 常见场 见场景 问题背景 题背景与 背景与常 景与常见 与常见场 常见场景 常出 出现 现在 在需 需要 要由 由后 后端 端访 问外 外部 部地 地址 址的 的功 功能 能中 常出现 出现在 现在需 在需要 需要由 要由后 由后端 后端访 端访问 访问外 问外部 外部地 部地址 地址的 址的功 的功能 功能中 常出现在 出现在需 现在需要 在需要由 需要由后 要由后端 由后端访 后端访问 端访问外 访问外部 问外部地 外部地址 部地址的 地址的功 址的功能 的功能中 例如 如图 图片 片远 远程 程导 导入 例如图 如图片 图片远 片远程 远程导 程导入 例如图片 如图片远 图片远程 片远程导 远程导入 在线 线截 截图 在线截 线截图 在线截图 文档 档转 转换 文档转 档转换 文档转换 预览 第三 三方 方接 接口 口代 代理 理和 和云 云资 源同 同步 第三方 三方接 方接口 接口代 口代理 代理和 理和云 和云资 云资源 资源同 源同步 第三方接 三方接口 方接口代 接口代理 口代理和 代理和云 理和云资 和云资源 云资源同 资源同步 典型 型代 码逻 逻辑 辑是 典型代 型代码 代码逻 码逻辑 逻辑是 典型代码 型代码逻 代码逻辑 码逻辑是 户提 提交 交一 用户提 户提交 提交一 交一个 用户提交 户提交一 提交一个 端使 使用 务端使 端使用 服务端使 务端使用 客户 户端 问该 该地 客户端 户端访 访问该 问该地 该地址 客户端访 户端访问 端访问该 访问该地 问该地址 再将 将状 状态 态码 再将状 将状态 状态码 再将状态 将状态码 容或 或处 处理 理结 结果 果返 返回 回给 内容或 容或处 或处理 处理结 理结果 结果返 果返回 返回给 回给用 应内容或 内容或处 容或处理 或处理结 处理结果 理结果返 结果返回 果返回给 返回给用 回给用户 险通 常来 来自 自以 以下 下几 几个 个方 方面 风险通 险通常 通常来 常来自 来自以 自以下 以下几 下几个 几个方 个方面 风险通常 险通常来 通常来自 常来自以 来自以下 自以下几 以下几个 下几个方 几个方面 攻击 击者 者可 可以 以控 控制
  20. 背景文件上传是 Java Web 项目里非常常见的功能,头像、附件、导入模板、富文本图片都会用到。它看起来业务属性很强,但安全边界也最容易被低估。很多项目只做了前端限制或简单判断后缀,最后导致任意文件写入、脚本文件落地、路径穿越,甚至配合解析配置形成远程代码执行。 这篇文章不讨论具体攻击站点,也不提供针对真实系统的利用流程,主要整理一次代码审计中常见的文件上传风险点和修复思路。重点放在如何识别问题、如何验证风险、如何把修复做成闭环。 典型场景审计对象通常是 Spring Boot 或传统 Spring MVC 项目,上传接口大致长这样:接收 MultipartFile,取原始文件名,判断后缀,然后拼接保存路径,最后返回访问 URL。 @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) throws IOException { String originalName = file.getOriginalFilename(); String suffix = originalName.substring(originalName.lastIndexOf(".")); if (!".jpg".equalsIgnoreCase(suffix) && !".png".equalsIgnoreCase(suffix)) { return "unsupported"; } String savePath = uploadDir + "/" + originalName; file.transferTo(new File(savePath)); return "/uploads/" + originalName; }从功能角度看,这段代码能跑;从安全角度看,它至少有几个问题:信任原始文件名、只依赖后缀、保存目录可能可执行、文件名可覆盖、缺少大小限制、缺少内容校验。 问题拆解1. 原始文件名不可信getOriginalFilename()来自客户端请求,不应直接作为服务端文件名使用。它可能包含特殊字符、路径分隔符、超长字符串,也可能造成同名覆盖。 常见风险包括: 路径穿越:文件名中包含 ../ 或编码变体时,可能写出预期目录。覆盖文件:使用原始文件名保存,可能覆盖已有资源。日志污染:文件名中包含换行、控制字符,影响日志排查。跨平台差异:Windows 与 Linux 对保留字符、大小写、路径分隔符的处理不同。审计时看到原始文件名直接参与路径拼接,需要重点标记。 2. 后缀校验不能代表文件安全很多项目只做类似 .jpg、.png 的后缀判断,但后缀是用户可控字段。更稳妥的做法是组合校验: 白名单后缀,而不是黑名单。服务端重新生成文件名,后缀由校验结果决定。校验 MIME 只能作为辅助,不能单独信任。读取文件头或使用成熟库判断文件类型。对图片类文件可尝试解码并重编码,去除附加内容。在 Java 项目里,可以用 Apache Tika、图片解码库或业务允许范围内的轻量实现做内容识别。 private static final Set<String> ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "gif"); private boolean isAllowedExt(String ext) { return ALLOWED_EXT.contains(ext.toLowerCase(Locale.ROOT)); }注意:扩展名校验只是一层门槛,不是完整防护。 3. 上传目录是否具备执行能力文件上传最危险的组合通常是“可写 + 可访问 + 可执行”。如果上传目录被 Web 容器当成动态脚本目录解析,风险会明显扩大。 安全设计上,应尽量满足: 上传文件保存到 Web 根目录之外。通过受控下载接口读取文件,而不是直接暴露真实路径。静态资源服务器禁止脚本解析。对象存储桶关闭不必要的公开写权限。上传目录配置独立域名时,避免携带主站 Cookie。Nginx 场景下,静态上传目录可以明确禁止脚本类文件解析,示例仅展示防护思路: location /uploads/ { alias /data/app/uploads/; autoindex off; default_type application/octet-stream; location ~* \.(php|jsp|jspx|asp|aspx)$ { return 403; } }如果是 Tomcat/Spring Boot,重点是不要把上传目录配置到可被 JSP 等动态引擎解析的位置。现代 Spring Boot 默认不解析 JSP 上传目录,但老项目、混合部署、反向代理错误配置仍然需要检查。 4. 路径拼接与规范化审计文件操作时,建议关注所有 new File(base, name)、Paths.get(base, name)、字符串拼接路径的代码。安全实现应先生成服务端文件名,再进行路径规范化校验,确保最终路径仍在允许目录下。 Path baseDir = Paths.get(uploadDir).toAbsolutePath().normalize(); Files.createDirectories(baseDir); String safeName = UUID.randomUUID() + "." + ext; Path target = baseDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("invalid upload path"); } try (InputStream in = file.getInputStream()) { Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); }这里的关键点不是 UUID 本身,而是“不要信任用户文件名”和“最终落点必须做目录边界校验”。 5. 文件大小、数量与资源消耗上传功能还常见拒绝服务风险,例如大文件占满磁盘、并发上传拖垮带宽、压缩包解压导致磁盘膨胀。即使没有代码执行风险,也可能造成业务不可用。 Spring Boot 中可以设置基础上传限制: spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=10MB但这只是入口限制。实际项目还应配合: 用户维度限频和配额。服务端磁盘水位监控。临时目录定期清理。异步处理队列限制并发。压缩包类文件限制层级、总大小、文件数量。如果业务允许上传压缩包,务必检查 Zip Slip 问题。解压时不能直接使用压缩包内的文件名作为落地路径。 Path destDir = Paths.get("/data/import").toAbsolutePath().normalize(); try (ZipInputStream zis = new ZipInputStream(inputStream)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { Path out = destDir.resolve(entry.getName()).normalize(); if (!out.startsWith(destDir)) { throw new SecurityException("zip entry path traversal"); } // 后续再根据业务处理文件,省略写入细节 } }审计关键步骤实际代码审计时,我通常按下面的顺序排查,效率比较高。 第一步:定位上传入口检索关键字: MultipartFile @RequestParam("file") CommonsMultipartFile ServletFileUpload getOriginalFilename transferTo Files.copy FileOutputStream除了 Controller,也要看富文本编辑器、导入功能、第三方插件目录。很多老项目的上传接口不在统一文件服务里,而是散落在各个业务模块。 第二步:确认文件保存位置重点判断: 是否保存到 Web 根目录内。是否能通过 URL 直接访问。是否存在静态映射配置。是否经过反向代理或 CDN 暴露。对象存储权限是否过宽。Spring MVC 中常见静态资源映射示例: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:/data/app/uploads/"); }这类配置本身不一定有问题,但需要结合文件类型校验、目录权限、下载策略一起看。 第三步:检查校验逻辑是否可绕开这里不是鼓励绕过,而是从防守角度确认校验是否建立在可信数据上。常见薄弱点包括: 只在前端限制文件类型。只判断 Content-Type。只用 contains 判断后缀。大小写、空格、特殊字符处理不一致。业务层和网关层规则不一致。上传后异步处理,前置校验与后置使用脱节。更稳妥的策略是:入口校验、内容识别、服务端重命名、保存目录隔离、访问出口控制同时存在。 第四步:验证风险影响面验证时建议在本地测试环境或授权环境完成。重点不是“能不能打”,而是证明风险链条是否成立: 非法类型文件是否能保存。保存路径是否可预测。文件是否可被外部访问。是否可能覆盖已有文件。是否可能写出指定目录。上传异常是否泄露绝对路径。验证结果最好记录为“前置条件 + 触发点 + 影响范围 + 修复建议”,这样研发更容易处理。 修复参考实现下面是一段相对完整的上传处理示例,适合作为修复方向参考。实际项目还要结合鉴权、审计日志、存储服务做调整。 public UploadResult uploadImage(MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("empty file"); } long maxSize = 5 * 1024 * 1024; if (file.getSize() > maxSize) { throw new IllegalArgumentException("file too large"); } String originalName = Optional.ofNullable(file.getOriginalFilename()).orElse(""); String ext = getExt(originalName); if (!isAllowedExt(ext)) { throw new IllegalArgumentException("unsupported file type"); } byte[] head = file.getInputStream().readNBytes(16); if (!looksLikeImage(head)) { throw new IllegalArgumentException("invalid image content"); } Path baseDir = Paths.get("/data/app/uploads/images").toAbsolutePath().normalize(); Files.createDirectories(baseDir); String safeName = LocalDate.now() + "-" + UUID.randomUUID() + "." + ext.toLowerCase(Locale.ROOT); Path target = baseDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("invalid path"); } try (InputStream in = file.getInputStream()) { Files.copy(in, target, StandardCopyOption.CREATE_NEW); } return new UploadResult(safeName, "/file/view/" + safeName); }需要注意,示例中的 looksLikeImage 只是占位逻辑。生产环境建议使用成熟库判断文件类型,图片场景可以进一步读取并重新编码,避免把不必要的附加数据原样保存。 下载接口也要一起看很多团队修上传时只盯着写入,但读取接口同样重要。下载接口如果按用户传入的文件名直接读取,也可能出现任意文件读取或越权访问。 @GetMapping("/file/view/{name}") public ResponseEntity<Resource> view(@PathVariable String name) throws IOException { if (!name.matches("^[a-zA-Z0-9._-]{1,120}$")) { return ResponseEntity.badRequest().build(); } Path baseDir = Paths.get("/data/app/uploads/images").toAbsolutePath().normalize(); Path target = baseDir.resolve(name).normalize(); if (!target.startsWith(baseDir) || !Files.exists(target)) { return ResponseEntity.notFound().build(); } Resource resource = new UrlResource(target.toUri()); return ResponseEntity.ok() .header("Content-Disposition", "inline; filename=\"" + name + "\"") .body(resource); }如果文件涉及用户隐私或业务数据,不能只靠文件名不可猜来保护,必须做对象级鉴权,例如校验当前用户是否有权限访问该文件记录。 日志与告警上传功能的安全日志容易被忽略。建议至少记录以下字段: 用户 ID 或调用方标识。上传接口路径。原始文件名的安全化版本。最终文件 ID 或存储 key。文件大小、识别类型。失败原因分类。请求来源 IP 和 Trace ID。日志里不要直接写入未清洗的原始文件名,避免控制字符污染日志。异常响应也不要返回服务端绝对路径、堆栈信息和存储目录。 常见误区误区问题建议只做前端限制请求可被直接构造服务端必须重新校验只看后缀后缀完全可控结合内容识别与白名单使用原始文件名覆盖、路径问题、日志污染服务端生成文件名上传目录放 Web 根目录可能被直接访问或解析存放到 Web 根目录外下载接口无鉴权可能越权读取按文件记录做权限校验总结文件上传风险很少是单点问题,更多是多个小疏忽串在一起:文件名可信、校验薄弱、目录暴露、下载无鉴权、日志不完整。审计时不要只问“能不能上传某类文件”,而要看完整链路:谁能上传、上传什么、存到哪里、如何访问、是否可执行、能否覆盖、是否可追踪。 比较稳妥的基线是:服务端白名单校验、内容识别、随机文件名、目录隔离、禁止脚本解析、大小与频率限制、下载鉴权、完整日志。做到这些,即使某一层出现疏漏,也不至于直接扩大为高危问题。 经验上看,上传功能最好作为公共能力统一收口。散落在各业务模块里的“临时上传接口”,往往才是后续安全事件的来源。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 项目 目里 里非 非常 常常 常见 见的 的功 功能 项目里 目里非 里非常 非常常 常常见 常见的 见的功 的功能 项目里非 目里非常 里非常常 非常常见 常常见的 常见的功 见的功能 头像 附件 导入 入模 模板 导入模 入模板 导入模板 富文 文本 本图 图片 片都 都会 会用 用到 富文本 文本图 本图片 图片都 片都会 都会用 会用到 富文本图 文本图片 本图片都 图片都会 片都会用 都会用到 它看 看起 起来 来业 业务 务属 属性 性很 很强 它看起 看起来 起来业 来业务 业务属 务属性 属性很 性很强 它看起来 看起来业 起来业务 来业务属 业务属性 务属性很 属性很强 但安 安全 全边 边界 界也 也最 最容 容易 易被 被低 低估 但安全 安全边 全边界 边界也 界也最 也最容 最容易 容易被 易被低 被低估 但安全边 安全边界 全边界也 边界也最 界也最容 也最容易 最容易被 容易被低 易被低估 很多 多项 目只 只做 做了 了前 前端 端限 限制 制或 或简 简单 单判 判断 断后 后缀 很多项 多项目 项目只 目只做 只做了 做了前 了前端 前端限 端限制 限制或 制或简 或简单 简单判 单判断 判断后 断后缀 很多项目 多项目只 项目只做 目只做了 只做了前 做了前端 了前端限 前端限制 端限制或 限制或简 制或简单 或简单判 简单判断 单判断后 判断后缀 最后 后导 导致 致任 任意 意文 件写 写入 最后导 后导致 导致任 致任意 任意文 意文件 文件写 件写入 最后导致 后导致任 导致任意 致任意文 任意文件 意文件写 文件写入 脚本 本文 件落 落地 脚本文 本文件 文件落 件落地 脚本文件 本文件落 文件落地 路径 径穿 穿越 路径穿 径穿越 路径穿越 甚至 至配 配合 合解 解析 析配 配置 置形 形成 成远 远程 程代 代码 码执 执行 甚至配 至配合 配合解 合解析 解析配 析配置 配置形 置形成 形成远 成远程 远程代 程代码 代码执 码执行 甚至配合 至配合解 配合解析 合解析配 解析配置 析配置形 配置形成 置形成远 形成远程 成远程代 远程代码 程代码执 代码执行 这篇 篇文 文章 章不 不讨 讨论 论具 具体 体攻 攻击 击站 站点 这篇文 篇文章 文章不 章不讨 不讨论 讨论具 论具体 具体攻 体攻击 攻击站 击站点 这篇文章 篇文章不 文章不讨 章不讨论 不讨论具 讨论具体 论具体攻 具体攻击 体攻击站 攻击站点 也不 不提 提供 供针 针对 对真 真实 实系 系统 统的 的利 利用 用流 流程 也不提 不提供 提供针 供针对 针对真 对真实 真实系 实系统 系统的 统的利 的利用 利用流 用流程 也不提供 不提供针 提供针对 供针对真 针对真实 对真实系 真实系统 实系统的 系统的利 统的利用 的利用流 利用流程 主要 要整 整理 理一 一次 次代 码审 审计 计中 中常 的文 传风 风险 险点 点和 和修 修复 复思 思路 主要整 要整理 整理一 理一次 一次代 次代码 代码审 码审计 审计中 计中常 中常见 见的文 的文件 上传风 传风险 风险点 险点和 点和修 和修复 修复思 复思路 主要整理 要整理一 整理一次 理一次代 一次代码 次代码审 代码审计 码审计中 审计中常 计中常见 中常见的 常见的文 见的文件 的文件上 件上传风 上传风险 传风险点 风险点和 险点和修 点和修复 和修复思 修复思路 重点 点放 放在 在如 如何 何识 识别 别问 问题 重点放 点放在 放在如 在如何 如何识 何识别 识别问 别问题 重点放在 点放在如 放在如何 在如何识 如何识别 何识别问 识别问题 何验 验证 证风 如何验 何验证 验证风 证风险 如何验证 何验证风 验证风险 何把 把修 复做 做成 成闭 闭环 如何把 何把修 把修复 修复做 复做成 做成闭 成闭环 如何把修 何把修复 把修复做 修复做成 复做成闭 做成闭环 典型 型场 场景 景审 计对 对象 象通 通常 常是 典型场 型场景 场景审 景审计 审计对 计对象 对象通 象通常 通常是 典型场景 型场景审 场景审计 景审计对 审计对象 计对象通 对象通常 象通常是 或传 传统 或传统 传接 接口 口大 大致 致长 长这 这样 上传接 传接口 接口大 口大致 大致长 致长这 长这样 上传接口 传接口大 接口大致 口大致长 大致长这 致长这样 接收 取原 原始 始文 件名 取原始 原始文 始文件 文件名 取原始文 原始文件 始文件名 然后
  21. 背景文件上传一直是 Web 安全里比较容易被低估的功能点。很多业务侧认为“只要限制后缀名就够了”,但在真实项目里,上传链路往往同时涉及前端校验、后端解析、对象存储、CDN 回源、图片处理组件、异步任务和访问控制。任何一个环节处理不一致,都可能把一个普通上传点变成风险入口。这篇文章不讨论攻击利用细节,也不提供绕过型 payload。主要结合日常代码审计和漏洞复现经验,整理一套适合研发、安全测试、应急排查使用的分析方法,重点放在如何识别风险、如何复现验证、如何修复和回归。典型场景常见的上传功能包括头像上传、附件上传、工单截图、富文本图片、导入模板、插件包上传等。风险通常不是来自“上传”这个动作本身,而是来自上传后的处理链路。上传文件是否被保存到 Web 可访问目录。服务端是否只依赖文件名后缀判断类型。是否允许用户控制保存路径、文件名或扩展名。是否对压缩包、文档、图片进行二次解析。下载或预览接口是否存在越权访问。对象存储桶权限是否过宽。异步处理任务是否存在命令拼接、模板注入或解析器漏洞风险。审计时不要只看上传接口,还要追踪文件从进入系统到被访问、处理、删除的完整生命周期。问题拆解一个较完整的上传链路通常包含以下几个步骤:客户端选择文件并发起请求。后端接收 multipart/form-data 请求。服务端进行类型、大小、数量、权限校验。生成存储路径和文件名。写入本地磁盘、对象存储或文件服务。返回文件 ID、URL 或访问 token。后续通过预览、下载、解析任务继续使用该文件。漏洞经常出现在“前后校验不一致”和“信任上游数据”这两类问题上。例如前端限制了图片格式,但后端没有校验;上传接口保存时做了后缀检查,但预览接口根据用户传入路径直接读取文件;对象存储私有桶设计成了公开读。技术分析审计上传功能时,建议从以下几个维度逐项确认。一、文件类型校验仅检查文件名后缀是不够的。更稳妥的做法是同时检查扩展名、Content-Type、文件头特征,并且以服务端判断为准。对于图片类文件,还可以通过安全的图片库重新解码再编码,丢弃原始文件中的额外内容。// Java 示例:仅作防御思路展示 private static final Set&lt;String&gt; ALLOWED_EXT = Set.of(\"jpg\", \"jpeg\", \"png\", \"gif\"); public boolean isAllowed(String originalName, String contentType) { String ext = getExt(originalName).toLowerCase(Locale.ROOT); if (!ALLOWED_EXT.contains(ext)) { return false; } if (!List.of(\"image/jpeg\", \"image/png\", \"image/gif\").contains(contentType)) { return false; } return true; }需要注意,Content-Type 可以由客户端控制,不能单独作为可信依据。文件头检查也不能覆盖全部场景,尤其是复杂文档、压缩包、多媒体文件,需要结合业务实际设置白名单。二、存储位置与访问方式如果上传文件直接落在 Web 根目录下,风险会明显增加。更推荐的方式是将文件存储在 Web 根目录之外,通过受控下载接口读取,并统一做权限校验、Content-Disposition、Content-Type 设置。location /uploads/ { # 不建议直接暴露可写目录 deny all; }下载接口也要避免直接信任用户传入的路径。更安全的设计是前端只拿到 fileId,后端根据 fileId 查询数据库中的存储位置,并验证当前用户是否有权限访问。// 推荐:通过 fileId 查询文件元数据 GET /api/file/download?id=12345 // 不推荐:由用户直接传路径 GET /download?path=/data/upload/2024/a.png三、文件名与路径处理用户上传的原始文件名不应直接作为最终存储文件名。建议使用随机 ID、UUID、哈希前缀等方式生成服务器端文件名,原始文件名只作为展示字段保存,并在展示时做 HTML 转义。String safeName = UUID.randomUUID().toString().replace(\"-\", \"\") + \".\" + ext; Path target = baseDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException(\"invalid path\"); }这里的 normalize 和 startsWith 检查很重要,能够降低路径穿越类问题的出现概率。尤其是在处理压缩包解压、批量导入、文件迁移任务时,路径拼接问题很常见。四、对象存储权限很多团队把文件上传迁移到对象存储后,会误以为风险随之消失。实际上对象存储更容易出现权限配置问题,例如桶公开读、临时凭证权限过大、上传策略未限制 key 前缀、回调接口未验签等。建议至少检查以下配置:默认使用私有桶,公开资源单独隔离。上传凭证限制文件大小、类型、路径前缀和有效期。临时凭证只授予必要动作,不给全桶写权限。服务端回调需要签名校验,不能只信任回调参数。敏感附件访问通过短期签名 URL 或后端转发。五、二次解析风险很多漏洞不是发生在上传瞬间,而是发生在上传后的解析阶段。例如图片压缩、文档转 PDF、压缩包解压、Excel 导入、音视频转码等。这些组件通常权限较高、调用链复杂,需要单独纳入安全评估。几个实用建议:解析任务使用低权限用户运行。任务进程放到容器或隔离环境中。限制文件大小、页数、分辨率、解压后总大小。设置超时时间和内存限制,避免资源耗尽。第三方解析库保持更新,关注对应 CVE 公告。# Linux 下可用 ulimit 给处理任务设置基础资源限制 ulimit -t 30 # CPU 时间限制 ulimit -v 524288 # 虚拟内存限制,单位 KB复现与验证思路在授权测试或内部自查时,建议按“最小化验证”原则进行,不做破坏性操作,不上传危险文件,不影响线上业务。可以使用无害样本验证校验逻辑是否生效。检查点验证方式预期结果后缀白名单上传非业务允许类型的普通文本文件服务端拒绝大小限制上传超过限制的测试文件服务端拒绝并返回明确错误权限控制使用 A 用户文件 ID 由 B 用户访问返回无权限路径控制观察下载接口是否接受 path 参数不允许用户直接指定物理路径对象存储检查桶 ACL 和上传策略权限最小化二次解析上传边界大小内的合法样本任务可控、超时可回收日志侧建议关注上传失败率、异常扩展名、同一账号短时间大量上传、解析任务频繁超时等行为。这些信息对发现异常使用很有帮助。关键修复建议服务端做强制白名单校验,不依赖前端。上传目录与 Web 根目录隔离。文件访问统一走鉴权接口,不直接暴露真实路径。保存文件名由服务端生成,原始文件名仅作展示。对象存储使用私有桶和最小权限临时凭证。图片、文档、压缩包等解析任务放到隔离环境。对上传、下载、预览、删除接口统一做权限校验。建立安全回归用例,避免后续功能迭代引入绕过。一个相对稳妥的上传流程1. 用户请求上传凭证或直接上传到后端 2. 后端验证用户身份和业务权限 3. 校验文件大小、扩展名、MIME、文件头 4. 生成服务端文件名和存储路径 5. 文件写入非 Web 根目录或私有对象存储 6. 记录 fileId、owner、hash、size、contentType、storageKey 7. 返回 fileId,不返回真实物理路径 8. 下载或预览时根据 fileId 查询并鉴权 9. 需要解析的文件进入隔离任务队列 10. 定期清理无引用文件和过期临时文件注意点不要把“上传成功”作为安全边界。真正需要关注的是文件能否被执行、能否被越权访问、能否影响解析组件、能否造成资源耗尽。错误信息不要回显服务器路径、堆栈、存储桶内部 key。富文本图片上传要单独限制域名和协议,避免混入外链风险。压缩包解压要防止路径穿越和解压膨胀。导入功能要限制单元格数量、公式处理和外部引用。删除文件要校验归属,避免任意删除。文件哈希可用于去重,但不要作为唯一权限判断依据。总结文件上传漏洞的本质不是某个单点校验缺失,而是文件生命周期管理不完整。比较稳的做法是:白名单校验、隔离存储、受控访问、最小权限、解析隔离、日志监控和持续回归。在代码审计时,建议沿着“上传入口、存储路径、访问接口、解析任务、对象存储权限”这条线完整走一遍。很多问题表面看是上传漏洞,实际根因可能是鉴权缺失、路径拼接不当、权限配置过宽或第三方组件使用不安全。把链路梳理清楚,修复才不会停留在简单加一个后缀判断。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传一 一直 直是 背景文 景文件 文件上 件上传 上传一 传一直 一直是 背景文件 景文件上 文件上传 件上传一 上传一直 传一直是 安全 全里 里比 比较 较容 容易 易被 被低 低估 估的 的功 功能 能点 安全里 全里比 里比较 比较容 较容易 容易被 易被低 被低估 低估的 估的功 的功能 功能点 安全里比 全里比较 里比较容 比较容易 较容易被 容易被低 易被低估 被低估的 低估的功 估的功能 的功能点 很多 多业 业务 务侧 侧认 认为 很多业 多业务 业务侧 务侧认 侧认为 很多业务 多业务侧 业务侧认 务侧认为 只要 要限 限制 制后 后缀 缀名 名就 就够 够了 只要限 要限制 限制后 制后缀 后缀名 缀名就 名就够 就够了 只要限制 要限制后 限制后缀 制后缀名 后缀名就 缀名就够 名就够了 但在 在真 真实 实项 项目 目里 但在真 在真实 真实项 实项目 项目里 但在真实 在真实项 真实项目 实项目里 传链 链路 路往 往往 往同 同时 时涉 涉及 及前 前端 端校 校验 上传链 传链路 链路往 路往往 往往同 往同时 同时涉 时涉及 涉及前 及前端 前端校 端校验 上传链路 传链路往 链路往往 路往往同 往往同时 往同时涉 同时涉及 时涉及前 涉及前端 及前端校 前端校验 后端 端解 解析 后端解 端解析 后端解析 对象 象存 存储 对象存 象存储 对象存储 回源 图片 片处 处理 理组 组件 图片处 片处理 处理组 理组件 图片处理 片处理组 处理组件 异步 步任 任务 务和 和访 访问 问控 控制 异步任 步任务 任务和 务和访 和访问 访问控 问控制 异步任务 步任务和 任务和访 务和访问 和访问控 访问控制 任何 何一 一个 个环 环节 节处 理不 不一 一致 任何一 何一个 一个环 个环节 环节处 节处理 处理不 理不一 不一致 任何一个 何一个环 一个环节 个环节处 环节处理 节处理不 处理不一 理不一致 都可 可能 能把 把一 个普 普通 通上 传点 点变 变成 成风 风险 险入 入口 都可能 可能把 能把一 把一个 一个普 个普通 普通上 通上传 上传点 传点变 点变成 变成风 成风险 风险入 险入口 都可能把 可能把一 能把一个 把一个普 一个普通 个普通上 普通上传 通上传点 上传点变 传点变成 点变成风 变成风险 成风险入 风险入口 这篇 篇文 文章 章不 不讨 讨论 论攻 攻击 击利 利用 用细 细节 这篇文 篇文章 文章不 章不讨 不讨论 讨论攻 论攻击 攻击利 击利用 利用细 用细节 这篇文章 篇文章不 文章不讨 章不讨论 不讨论攻 讨论攻击 论攻击利 攻击利用 击利用细 利用细节 也不 不提 提供 供绕 绕过 过型 也不提 不提供 提供绕 供绕过 绕过型 也不提供 不提供绕 提供绕过 供绕过型 主要 要结 结合 合日 日常 常代 代码 码审 审计 计和 和漏 漏洞 洞复 复现 现经 经验 主要结 要结合 结合日 合日常 日常代 常代码 代码审 码审计 审计和 计和漏 和漏洞 漏洞复 洞复现 复现经 现经验 主要结合 要结合日 结合日常 合日常代 日常代码 常代码审 代码审计 码审计和 审计和漏 计和漏洞 和漏洞复 漏洞复现 洞复现经 复现经验 整理 理一 一套 套适 适合 合研 研发 整理一 理一套 一套适 套适合 适合研 合研发 整理一套 理一套适 一套适合 套适合研 适合研发 全测 测试 安全测 全测试 安全测试 应急 急排 排查 查使 使用 用的 的分 分析 析方 方法 应急排 急排查 排查使 查使用 使用的 用的分 的分析 分析方 析方法 应急排查 急排查使 排查使用 查使用的 使用的分 用的分析 的分析方 分析方法 重点 点放 放在 在如 如何 何识 识别 别风 重点放 点放在 放在如 在如何 如何识 何识别 识别风 别风险 重点放在 点放在如 放在如何 在如何识 如何识别 何识别风 识别风险 何复 现验 验证 如何复 何复现 复现验 现验证 如何复现 何复现验 复现验证 何修 修复 复和 和回 回归 如何修 何修复 修复和 复和回 和回归 如何修复 何修复和 修复和回 复和回归 典型 型场 场景 景常 常见 见的 的上 传功 能包 包括 括头 头像 像上 典型场 型场景 场景常 景常见 常见的 见的上 的上传 上传功 传功能 功能包 能包括 包括头 括头像 头像上 像上传 典型场景 型场景常 场景常见 景常见的 常见的上 见的上传 的上传功 上传功能 传功能包 功能包括 能包括头 包括头像 括头像上 头像上传 附件 附件上 附件上传 工单 单截 截图 工单截
  22. 背景文件上传是 Web 系统里很常见的功能,头像、附件、工单截图、富文本图片都会用到。它看起来只是业务能力,但在安全审计里一直属于高风险入口:一旦校验不足,可能导致脚本文件落地、解析规则误用、敏感文件覆盖、存储桶公开访问等问题。 这篇整理来自几次代码审计和复盘里的共性问题,重点放在“如何发现风险”和“如何把上传链路做稳”。不讨论未授权入侵、恶意控制等内容,只从防护和验证角度展开。 常见场景与风险点一个典型上传链路大致包括:前端选择文件、后端接收 multipart 请求、校验文件、保存到本地或对象存储、返回访问 URL。问题通常不出在某一个点,而是多个小疏忽叠加。 只校验文件后缀,未校验真实文件类型。使用用户提交的原始文件名保存,存在路径穿越或覆盖风险。上传目录位于 Web 可执行路径下,被服务器按脚本解析。图片处理库未限制尺寸,导致内存消耗异常。对象存储权限配置过宽,上传文件被公开列举或任意读取。业务接口缺少鉴权或额度限制,被滥用为临时网盘。审计时建议先看入口代码审计时,我一般先定位接收 multipart 的接口,再看三个问题:谁能传、能传什么、传到哪里。很多漏洞不是因为某段代码特别复杂,而是因为入口默认信任了用户输入。 以 Java Spring 项目为例,可以先全局搜索以下关键词: MultipartFile @RequestParam("file") transferTo( getOriginalFilename() Files.copy( putObject( uploadPHP 项目可以关注: $_FILES move_uploaded_file pathinfo mime_content_type getimagesizeNode.js 项目可以关注: multer busboy formidable originalname mimetype destination filename找到上传入口后,不要只看控制器层。实际保存逻辑经常封装在 FileService、StorageClient、OssUtil 之类的工具类里,真正的风险点往往在这些通用组件中。 技术分析:几个容易被忽略的细节1. 后缀白名单不是唯一依据后缀校验必须做,但不能只依赖后缀。用户提交的 filename、Content-Type 都可以伪造。更稳妥的做法是:后缀白名单、魔数识别、解码验证三者结合。 例如图片上传,至少应确认: 扩展名只允许 jpg、jpeg、png、gif、webp 等明确类型。文件头符合预期格式。可以被安全图片库正常解码。重新编码后再保存,避免保留异常附加内容。如果业务只需要展示图片,推荐“解码后重编码”而不是原样保存。这样可以显著降低伪造文件和异常结构文件带来的风险。 2. 文件名必须由服务端生成不要直接使用 getOriginalFilename() 或 originalname 作为最终落盘文件名。原始文件名最多用于展示,保存时应由服务端生成随机名,并去掉路径语义。 // 示例:Java 中生成安全文件名思路 String ext = getSafeExtension(originalName); String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path target = uploadRoot.resolve(fileName).normalize(); if (!target.startsWith(uploadRoot)) { throw new SecurityException("invalid upload path"); }这里有两个点比较关键:一是文件名不可预测,二是 normalize 后必须确认最终路径仍在预期目录内,避免路径穿越。 3. 上传目录不要具备脚本执行能力如果文件保存到 Web 根目录下,一定要确认服务器不会把该目录中的文件当作脚本执行。更推荐的方案是:上传文件存放在 Web 根目录之外,通过静态资源服务或对象存储读取。 Nginx 场景下,可以单独给上传目录设置只读静态访问,并禁止脚本解析: location /uploads/ { alias /data/app/uploads/; autoindex off; add_header X-Content-Type-Options nosniff; types { image/jpeg jpg jpeg; image/png png; image/gif gif; image/webp webp; application/pdf pdf; } default_type application/octet-stream; } # 不要在 uploads 路径下配置 PHP、JSP、CGI 等动态解析如果是对象存储,建议区分上传桶和分发桶,或者通过后端审核、转码后再发布到可访问区域。 4. MIME 类型要服务端控制浏览器访问上传文件时,如果响应头不准确,可能引发内容嗅探问题。建议显式设置 Content-Type,并加上 X-Content-Type-Options: nosniff。 对于不需要浏览器直接打开的附件,可以统一使用下载头: Content-Type: application/octet-stream Content-Disposition: attachment; filename="safe-name.ext" X-Content-Type-Options: nosniff这类细节经常被忽略,但在富文本附件、工单附件、站内消息附件场景里很实用。 5. 图片处理要限制尺寸和资源消耗很多系统会在上传后生成缩略图。如果未限制图片像素、帧数、文件大小,可能被异常大图拖垮处理进程。这里不需要复杂技巧,只要缺少限制就可能出问题。 限制单文件大小,例如头像 2MB、普通图片 10MB。限制图片宽高和总像素,例如不超过 8000x8000。限制 GIF/WebP 动图帧数或直接禁止动图。图片处理放到异步队列,避免阻塞主请求线程。处理失败时删除临时文件,避免垃圾文件堆积。关键步骤:一套比较稳的上传处理流程实际落地时,可以按下面的流程实现,不必依赖单一校验点。 接口鉴权:确认用户身份和业务权限。额度限制:限制频率、单文件大小、每日总量。基础校验:检查扩展名、文件大小、文件数量。内容识别:通过文件头和安全库判断真实类型。安全处理:图片重编码,文档走杀毒或沙箱扫描。生成文件名:服务端生成随机名,避免使用原始文件名。隔离存储:上传目录不可执行,最好不在 Web 根目录。返回引用:返回文件 ID 或受控 URL,不直接暴露内部路径。访问控制:下载接口校验权限,敏感附件不要公开访问。日志审计:记录用户、IP、文件摘要、大小、类型、结果。如果论坛编辑器不支持有序列表,上面这套也可以理解为从“入口控制”到“访问控制”的闭环。重点是不要让上传后的文件脱离业务权限体系。 一个简化版安全校验示例下面是偏伪代码的 Java 示例,主要表达处理思路,具体库需要按项目情况替换。 public UploadResult uploadImage(MultipartFile file, User user) throws Exception { if (user == null) { throw new SecurityException("unauthorized"); } if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) { throw new IllegalArgumentException("invalid file size"); } String originalName = file.getOriginalFilename(); String ext = getSafeExtension(originalName); if (!List.of("jpg", "jpeg", "png", "webp").contains(ext)) { throw new IllegalArgumentException("unsupported extension"); } byte[] bytes = file.getBytes(); String detectedType = detectByMagic(bytes); if (!isAllowedImageType(detectedType, ext)) { throw new IllegalArgumentException("invalid file type"); } BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes)); if (image == null) { throw new IllegalArgumentException("image decode failed"); } long pixels = (long) image.getWidth() * image.getHeight(); if (pixels > 40_000_000L) { throw new IllegalArgumentException("image too large"); } String safeName = UUID.randomUUID().toString().replace("-", "") + ".jpg"; Path target = uploadRoot.resolve(safeName).normalize(); if (!target.startsWith(uploadRoot)) { throw new SecurityException("invalid path"); } // 重编码保存,避免原样落盘 ImageIO.write(image, "jpg", target.toFile()); String sha256 = sha256(target); auditLog(user.getId(), safeName, sha256, file.getSize(), detectedType); return new UploadResult(safeName, "/media/" + safeName); }这里把最终文件统一转成 jpg,只是示例。实际业务如果需要透明背景,就要对 png 做专门处理。关键点是:不要把用户上传的字节流在校验不足的情况下直接放到可访问目录。 审计清单检查项常见问题建议鉴权上传接口匿名可访问接入登录态和业务权限校验类型校验只看后缀或 Content-Type后缀、魔数、解码验证结合文件名使用原始文件名保存服务端生成随机名存储路径位于可执行 Web 目录隔离存储,禁止脚本解析访问控制拿到 URL 即可访问敏感文件走鉴权下载资源限制未限制大小、像素、频率设置大小、尺寸、频率和总量限制日志无法追踪上传来源记录用户、IP、摘要、类型、结果测试验证建议加固完成后,建议做一轮回归验证。这里的验证目标是确认防护是否生效,不涉及攻击利用。 上传允许类型文件,确认正常保存和访问。上传不在白名单内的扩展名,确认被拒绝。修改 Content-Type 后上传,确认服务端不受影响。使用异常文件名,如包含路径分隔符,确认不会影响保存路径。上传超大文件、超大尺寸图片,确认被限制。访问上传目录下不存在的文件,确认不会列目录。检查响应头,确认存在 nosniff 等必要安全头。如果项目使用对象存储,还要额外检查 Bucket ACL、临时凭证权限、回调鉴权、跨域规则。很多线上问题不是应用代码导致的,而是云存储权限过宽。 注意点上传安全不要寄希望于某个单点规则。比较可靠的做法是:入口鉴权、内容校验、存储隔离、访问控制、日志审计同时具备。前端校验只能改善体验,不能作为安全边界。黑名单策略不可靠,优先使用明确白名单。不要把内部绝对路径返回给前端。临时文件要及时清理,失败流程也要覆盖。富文本图片和普通附件最好使用不同策略。敏感附件不要放公共 CDN,至少使用短期签名 URL。总结文件上传漏洞的根源通常是“把用户输入当成可信文件”以及“上传后文件进入了不该进入的执行或公开访问环境”。审计时抓住三个问题:谁能传、能传什么、传到哪里,基本可以定位大部分风险。 加固也不复杂:白名单、真实类型识别、服务端命名、重编码、存储隔离、下载鉴权、资源限制和日志审计。真正难的是在业务迭代中保持这些规则不被绕开,所以建议把上传能力收敛到统一组件,避免每个业务线各写一套。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 系统 统里 里很 很常 常见 见的 的功 功能 系统里 统里很 里很常 很常见 常见的 见的功 的功能 系统里很 统里很常 里很常见 很常见的 常见的功 见的功能 头像 附件 工单 单截 截图 工单截 单截图 工单截图 富文 文本 本图 图片 片都 都会 会用 用到 富文本 文本图 本图片 图片都 片都会 都会用 会用到 富文本图 文本图片 本图片都 图片都会 片都会用 都会用到 它看 看起 起来 来只 只是 是业 业务 务能 能力 它看起 看起来 起来只 来只是 只是业 是业务 业务能 务能力 它看起来 看起来只 起来只是 来只是业 只是业务 是业务能 业务能力 但在 在安 安全 全审 审计 计里 里一 一直 直属 属于 于高 高风 风险 险入 入口 但在安 在安全 安全审 全审计 审计里 计里一 里一直 一直属 直属于 属于高 于高风 高风险 风险入 险入口 但在安全 在安全审 安全审计 全审计里 审计里一 计里一直 里一直属 一直属于 直属于高 属于高风 于高风险 高风险入 风险入口 一旦 旦校 校验 验不 不足 一旦校 旦校验 校验不 验不足 一旦校验 旦校验不 校验不足 可能 能导 导致 致脚 脚本 本文 件落 落地 可能导 能导致 导致脚 致脚本 脚本文 本文件 文件落 件落地 可能导致 能导致脚 导致脚本 致脚本文 脚本文件 本文件落 文件落地 解析 析规 规则 则误 误用 解析规 析规则 规则误 则误用 解析规则 析规则误 规则误用 敏感 感文 件覆 覆盖 敏感文 感文件 文件覆 件覆盖 敏感文件 感文件覆 文件覆盖 存储 储桶 桶公 公开 开访 访问 问等 等问 问题 存储桶 储桶公 桶公开 公开访 开访问 访问等 问等问 等问题 存储桶公 储桶公开 桶公开访 公开访问 开访问等 访问等问 问等问题 这篇 篇整 整理 理来 来自 自几 几次 次代 代码 码审 计和 和复 复盘 盘里 里的 的共 共性 性问 这篇整 篇整理 整理来 理来自 来自几 自几次 几次代 次代码 代码审 码审计 审计和 计和复 和复盘 复盘里 盘里的 里的共 的共性 共性问 性问题 这篇整理 篇整理来 整理来自 理来自几 来自几次 自几次代 几次代码 次代码审 代码审计 码审计和 审计和复 计和复盘 和复盘里 复盘里的 盘里的共 里的共性 的共性问 共性问题 重点 点放 放在 重点放 点放在 重点放在 如何 何发 发现 现风 如何发 何发现 发现风 现风险 如何发现 何发现风 发现风险 何把 把上 传链 链路 路做 做稳 如何把 何把上 把上传 上传链 传链路 链路做 路做稳 如何把上 何把上传 把上传链 上传链路 传链路做 链路做稳 不讨 讨论 论未 未授 授权 权入 入侵 不讨论 讨论未 论未授 未授权 授权入 权入侵 不讨论未 讨论未授 论未授权 未授权入 授权入侵 恶意 意控 控制 制等 等内 内容 恶意控 意控制 控制等 制等内 等内容 恶意控制 意控制等 控制等内 制等内容 只从 从防 防护 护和 和验 验证 证角 角度 度展 展开 只从防 从防护 防护和 护和验 和验证 验证角 证角度 角度展 度展开 只从防护 从防护和 防护和验 护和验证 和验证角 验证角度 证角度展 角度展开 见场 场景 景与 与风 险点 点一 一个 个典 典型 型上 路大 大致 致包 包括 常见场 见场景 场景与 景与风 与风险 风险点 险点一 点一个 一个典 个典型 典型上 型上传 链路大 路大致 大致包 致包括 常见场景 见场景与 场景与风 景与风险 与风险点 风险点一 险点一个 点一个典 一个典型 个典型上 典型上传 型上传链 传链路大 链路大致 路大致包 大致包括 前端 端选 选择 择文 前端选 端选择 选择文 择文件 前端选择 端选择文 选择文件 后端 端接 接收 后端接 端接收 后端接收 请求 验文 校验文 验文件 校验文件 保存 存到 到本 本地 地或 或对 对象 象存 保存到 存到本 到本地 本地或 地或对 或对象 对象存 象存储 保存到本 存到本地 到本地或 本地或对 地或对象 或对象存 对象存储 返回 回访 返回访 回访问 返回访问 题通 通常 常不 不出 出在 在某 某一 个点 问题通 题通常 通常不 常不出 不出在 出在某 在某一 某一个 一个点 问题通常 题通常不 通常不出 常不出在 不出在某 出在某一 在某一个 某一个点 而是 是多 多个 个小 小疏 疏忽 忽叠 叠加 而是多 是多个 多个小 个小疏 小疏忽 疏忽叠 忽叠加
  23. 背景接口鉴权问题在代码审计里非常常见,但实际危害往往被低估。很多系统表面上有登录态、网关鉴权、角色菜单控制,真正落到业务接口时却只做了“是否登录”的判断,没有验证当前用户是否有权限访问目标资源。 这类问题不一定表现为传统意义上的漏洞利用链,但在真实业务中影响很直接:越权查看订单、修改他人资料、导出不属于自己的数据,甚至触发后台流程。本文整理一次典型接口鉴权缺陷的审计思路,重点放在如何发现、确认和修复,避免提供攻击性操作引导。 问题场景审计对象是一个常见的 Java Web 后台系统,技术栈大致如下: Spring Boot + Spring MVCJWT 登录态MyBatis 访问数据库前端基于菜单权限展示不同页面部分接口在网关层校验 Token 是否有效系统中存在多个资源型接口,例如订单、工单、客户资料、附件下载等。前端菜单权限做得比较完整,但后端部分接口只依赖前端传入的 ID 查询数据,没有绑定当前登录用户或组织范围。 经验上看,越权问题最容易出现在“详情查询、列表导出、附件下载、状态修改”这四类接口里。菜单权限只能控制入口,不能替代服务端资源级鉴权。技术分析审计时先看全局鉴权链路。系统使用拦截器解析 JWT,并把用户信息写入上下文: public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); LoginUser user = jwtService.parse(token); UserContext.set(user); return true; } }这段逻辑解决的是“用户是谁”,但没有解决“用户是否可以访问当前资源”。接下来需要沿着 Controller、Service、Mapper 一层层看业务接口是否做了资源归属校验。 一个有问题的接口大致类似下面这样: @GetMapping("/order/detail") public OrderDetailVO detail(@RequestParam Long orderId) { return orderService.getDetail(orderId); }public OrderDetailVO getDetail(Long orderId) { Order order = orderMapper.selectById(orderId); return convert(order); }这里的风险点比较明显:接口只根据 orderId 查询订单,没有使用当前登录用户信息,也没有校验组织、租户、数据范围。只要调用者处于登录状态,就可能访问不属于自己的订单详情。 再看 Mapper 层: <select id="selectById" resultType="Order"> SELECT id, user_id, org_id, amount, status, created_at FROM t_order WHERE id = #{id} </select>SQL 也没有任何数据范围条件。此时不需要复杂测试,单从代码路径已经可以判断资源级鉴权缺失。 审计关键步骤1. 梳理鉴权边界首先明确系统有哪些鉴权点,常见包括: 网关层:校验 Token、基础路由权限拦截器:解析用户身份、检查登录状态注解权限:如 @PreAuthorize、@RequiresPermissions业务层:校验数据归属、组织范围、角色可见范围数据库层:通过 tenant_id、org_id、user_id 限定查询范围只要某类接口没有落到业务层或数据库层的数据范围控制,就需要重点关注。 2. 关注 ID 直查类接口审计中可以优先搜索以下模式: selectById( getById( findById( detail( download( export( updateStatus( deleteById(这些方法名本身不代表一定有问题,但通常意味着“客户端传入资源 ID,服务端直接操作资源”。如果没有结合当前用户上下文,就容易出现越权。 重点检查 Controller 是否接收了资源 ID: @RequestParam Long id @PathVariable Long id @RequestBody XxxRequest request然后跟进 Service 是否有类似判断: LoginUser user = UserContext.get(); // 是否校验 userId / orgId / tenantId / role / dataScope如果完全没有使用当前用户信息,基本就要标记为高风险点。 3. 区分功能权限和数据权限很多系统会在方法上加权限注解,例如: @PreAuthorize("hasAuthority('order:view')") @GetMapping("/order/detail") public OrderDetailVO detail(@RequestParam Long orderId) { return orderService.getDetail(orderId); }这只能说明当前用户拥有“查看订单”这个功能权限,并不代表他可以查看任意订单。功能权限和数据权限不是一回事。 比较稳妥的写法是把资源归属也纳入查询条件: public OrderDetailVO getDetail(Long orderId) { LoginUser user = UserContext.get(); Order order = orderMapper.selectVisibleOrder( orderId, user.getTenantId(), user.getOrgId(), user.getUserId() ); if (order == null) { throw new BizException("资源不存在或无权限访问"); } return convert(order); }<select id="selectVisibleOrder" resultType="Order"> SELECT id, user_id, org_id, tenant_id, amount, status, created_at FROM t_order WHERE id = #{orderId} AND tenant_id = #{tenantId} AND ( user_id = #{userId} OR org_id = #{orgId} ) </select>实际项目中数据范围可能更复杂,例如部门树、项目成员、客户归属、审批参与人等,但原则一样:查询条件必须体现当前用户的可见范围。 4. 检查批量操作和导出接口批量接口比详情接口更容易被忽略。例如: @PostMapping("/order/export") public void export(@RequestBody OrderQuery query, HttpServletResponse response) { orderService.export(query, response); }如果 OrderQuery 中包含 orgId、userId、status、dateRange 等字段,后端不能直接信任前端传入的数据范围。正确做法是以后端用户上下文重新计算查询范围,而不是使用请求参数决定权限边界。 public List<Order> queryOrders(OrderQuery query) { LoginUser user = UserContext.get(); DataScope scope = dataScopeService.buildScope(user); return orderMapper.queryOrders(query, scope); }导出接口还应增加审计日志和数量限制,避免因为一个鉴权缺陷放大成大规模数据泄露。 5. 附件下载接口单独处理附件下载常被当成静态资源处理,但很多附件本质上属于业务对象,例如合同、工单截图、证件材料等。常见问题是文件表只根据 fileId 查询路径,然后直接输出文件。 public void download(Long fileId, HttpServletResponse response) { FileMeta file = fileMapper.selectById(fileId); fileStorage.writeToResponse(file.getPath(), response); }更安全的方式是附件表记录业务类型和业务 ID,下载时回到业务对象上做权限判断: public void download(Long fileId, HttpServletResponse response) { FileMeta file = fileMapper.selectById(fileId); if (file == null) { throw new BizException("文件不存在"); } permissionService.checkBusinessResource( file.getBizType(), file.getBizId(), UserContext.get() ); fileStorage.writeToResponse(file.getPath(), response); }这里不建议把“文件是否存在”和“无权限”区分得太明显,避免泄露资源枚举信息。 修复建议针对这类问题,单点修补通常不够,建议从以下几层一起改。 1. 建立统一的数据权限模型不要在每个 Service 里临时拼权限判断。可以抽象出 DataScope 或 PermissionContext,统一描述当前用户可访问的数据范围。 public class DataScope { private Long tenantId; private Set<Long> visibleOrgIds; private Set<Long> visibleUserIds; private boolean admin; }不同业务模块使用同一套上下文,能减少漏判,也方便后续审计。 2. 查询时默认带租户和组织条件多租户系统尤其要注意 tenant_id。很多事故不是因为没有角色权限,而是因为跨租户查询条件缺失。 WHERE tenant_id = #{scope.tenantId} AND org_id IN <foreach collection="scope.visibleOrgIds" item="orgId" open="(" close=")" separator=","> #{orgId} </foreach>如果使用 MyBatis Plus、JPA 或自研 ORM,也可以考虑在拦截器层统一注入租户条件,但业务数据权限仍建议在业务层明确表达,避免隐式规则难以排查。 3. 修改类接口要先查权限再操作状态修改、删除、审批等接口不能只校验目标 ID 是否存在。推荐流程是: 根据当前用户和数据范围查询目标资源判断资源状态是否允许操作执行业务变更记录操作审计日志public void cancelOrder(Long orderId) { LoginUser user = UserContext.get(); Order order = orderMapper.selectVisibleOrder( orderId, user.getTenantId(), user.getOrgId(), user.getUserId() ); if (order == null) { throw new BizException("资源不存在或无权限访问"); } if (!order.canCancel()) { throw new BizException("当前状态不允许取消"); } orderMapper.updateStatus(orderId, OrderStatus.CANCELED); auditLogService.record(user, "ORDER_CANCEL", orderId); }4. 增加自动化测试用例越权问题很适合写集成测试。至少准备两个普通用户,分别创建资源,然后验证用户 A 不能访问用户 B 的资源。 @Test void userShouldNotReadOthersOrder() { String tokenA = loginAs("userA"); Long orderB = createOrderAs("userB"); mockMvc.perform(get("/order/detail") .header("Authorization", tokenA) .param("orderId", String.valueOf(orderB))) .andExpect(status().isForbidden()); }如果系统为了兼容返回 200 + 业务错误码,也应在测试里明确断言错误码,避免出现“查不到”和“没权限”逻辑混乱。 审计时的注意点不要只看 Controller 上有没有权限注解,还要看资源归属是否校验。不要信任前端传入的 userId、orgId、tenantId,这些字段只能作为查询条件的一部分,不能作为权限依据。列表接口要关注导出、分页、搜索条件组合,很多越权只在复杂筛选时出现。附件、消息、审批记录、操作日志也属于敏感资源,不应按普通静态文件处理。管理员角色要有明确边界,避免一个“后台管理员”默认拥有跨租户或全局数据权限。错误信息保持克制,不建议返回“该资源属于其他用户”这类提示。风险分级参考接口类型常见问题风险关注点详情查询仅按 ID 查询敏感字段泄露列表导出数据范围由前端传入批量数据泄露状态修改只校验登录态越权变更业务流程附件下载fileId 直查路径合同、证件、工单材料泄露删除接口按 ID 直接删除数据破坏和审计缺失总结接口鉴权缺陷的核心不是“有没有登录”,而是“当前用户是否有权访问这个具体资源”。在代码审计中,建议围绕 ID 直查、批量导出、附件下载、状态修改这几类接口重点排查,沿着 Controller 到 Mapper 完整跟踪数据范围是否落地。 修复时不要依赖前端控制,也不要只补几个 if 判断。更稳妥的方案是建立统一数据权限模型,在查询和修改路径上都绑定当前用户上下文,同时用集成测试固化边界。这样后续业务迭代时,即使接口数量增加,也能降低同类问题反复出现的概率。 代码审计、调用链与关键函数定位示意 背景 景接 接口 口鉴 鉴权 权问 问题 题在 在代 代码 码审 审计 计里 里非 非常 常常 常见 背景接 景接口 接口鉴 口鉴权 鉴权问 权问题 问题在 题在代 在代码 代码审 码审计 审计里 计里非 里非常 非常常 常常见 背景接口 景接口鉴 接口鉴权 口鉴权问 鉴权问题 权问题在 问题在代 题在代码 在代码审 代码审计 码审计里 审计里非 计里非常 里非常常 非常常见 但实 实际 际危 危害 害往 往往 往被 被低 低估 但实际 实际危 际危害 危害往 害往往 往往被 往被低 被低估 但实际危 实际危害 际危害往 危害往往 害往往被 往往被低 往被低估 很多 多系 系统 统表 表面 面上 上有 有登 登录 录态 很多系 多系统 系统表 统表面 表面上 面上有 上有登 有登录 登录态 很多系统 多系统表 系统表面 统表面上 表面上有 面上有登 上有登录 有登录态 网关 关鉴 网关鉴 关鉴权 网关鉴权 角色 色菜 菜单 单控 控制 角色菜 色菜单 菜单控 单控制 角色菜单 色菜单控 菜单控制 真正 正落 落到 到业 业务 务接 口时 时却 却只 只做 做了 真正落 正落到 落到业 到业务 业务接 务接口 接口时 口时却 时却只 却只做 只做了 真正落到 正落到业 落到业务 到业务接 业务接口 务接口时 接口时却 口时却只 时却只做 却只做了 是否 否登 是否登 否登录 是否登录 的判 判断 的判断 没有 有验 验证 证当 当前 前用 用户 户是 否有 有权 权限 限访 访问 问目 目标 标资 资源 没有验 有验证 验证当 证当前 当前用 前用户 用户是 户是否 是否有 否有权 有权限 权限访 限访问 访问目 问目标 目标资 标资源 没有验证 有验证当 验证当前 证当前用 当前用户 前用户是 用户是否 户是否有 是否有权 否有权限 有权限访 权限访问 限访问目 访问目标 问目标资 目标资源 这类 类问 题不 不一 一定 定表 表现 现为 为传 传统 统意 意义 义上 上的 的漏 漏洞 洞利 利用 用链 这类问 类问题 问题不 题不一 不一定 一定表 定表现 表现为 现为传 为传统 传统意 统意义 意义上 义上的 上的漏 的漏洞 漏洞利 洞利用 利用链 这类问题 类问题不 问题不一 题不一定 不一定表 一定表现 定表现为 表现为传 现为传统 为传统意 传统意义 统意义上 意义上的 义上的漏 上的漏洞 的漏洞利 漏洞利用 洞利用链 但在 在真 真实 实业 务中 中影 影响 响很 很直 直接 但在真 在真实 真实业 实业务 业务中 务中影 中影响 影响很 响很直 很直接 但在真实 在真实业 真实业务 实业务中 业务中影 务中影响 中影响很 影响很直 响很直接 越权 权查 查看 看订 订单 越权查 权查看 查看订 看订单 越权查看 权查看订 查看订单 修改 改他 他人 人资 资料 修改他 改他人 他人资 人资料 修改他人 改他人资 他人资料 导出 出不 不属 属于 于自 自己 己的 的数 数据 导出不 出不属 不属于 属于自 于自己 自己的 己的数 的数据 导出不属 出不属于 不属于自 属于自己 于自己的 自己的数 己的数据 甚至 至触 触发 发后 后台 台流 流程 甚至触 至触发 触发后 发后台 后台流 台流程 甚至触发 至触发后 触发后台 发后台流 后台流程 本文 文整 整理 理一 一次 次典 典型 型接 权缺 缺陷 陷的 的审 计思 思路 本文整 文整理 整理一 理一次 一次典 次典型 典型接 型接口 鉴权缺 权缺陷 缺陷的 陷的审 的审计 审计思 计思路 本文整理 文整理一 整理一次 理一次典 一次典型 次典型接 典型接口 型接口鉴 口鉴权缺 鉴权缺陷 权缺陷的 缺陷的审 陷的审计 的审计思 审计思路 重点 点放 放在 在如 如何 何发 发现 重点放 点放在 放在如 在如何 如何发 何发现 重点放在 点放在如 放在如何 在如何发 如何发现 确认 认和 和修 修复 确认和 认和修 和修复 确认和修 认和修复 避免 免提 提供 供攻 攻击 击性 性操 操作 作引 引导 避免提 免提供 提供攻 供攻击 攻击性 击性操 性操作 操作引 作引导 避免提供 免提供攻 提供攻击 供攻击性 攻击性操 击性操作 性操作引 操作引导 题场 场景 景审 计对 对象 象是 是一 一个 个常 见的 问题场 题场景 场景审 景审计 审计对 计对象 对象是 象是一 是一个 一个常 个常见 常见的 问题场景 题场景审 场景审计 景审计对 审计对象 计对象是 对象是一 象是一个 是一个常 一个常见 个常见的 台系 后台系 台系统 后台系统 技术 术栈 栈大 大致 致如 如下 技术栈 术栈大
  24. 背景文件上传是 Web 系统里很常见的功能,头像、附件、工单截图、文档导入都会用到。也正因为入口常见、数据不可控,上传点一直是代码审计和渗透测试中优先关注的位置。这篇文章结合我在审计业务系统时经常遇到的问题,整理一套相对完整的分析思路和防护链路。重点不是讲“如何利用”,而是说明为什么某些写法不可靠,以及在工程上如何把风险压下去。常见问题场景很多上传漏洞并不是单点失误,而是多个环节同时薄弱造成的。常见场景包括:仅通过前端限制文件后缀,后端没有重新校验。后端只判断文件名后缀,没有校验 MIME、文件头、内容特征。上传目录位于 Web 可访问路径下,并且服务器可能解析动态脚本。文件名直接使用用户输入,导致路径穿越、覆盖文件或特殊字符问题。图片处理、压缩、解压等二次处理逻辑缺少边界控制。权限控制缺失,普通用户可以上传到全站公共目录。在真实项目中,最危险的情况通常是“上传目录可访问 + 文件类型校验薄弱 + Web 容器存在错误解析配置”。这类组合风险需要从代码和部署两侧一起看。技术分析先看一段比较典型的问题代码,示例使用 Java Spring 风格,其他语言也类似。public String upload(MultipartFile file) throws IOException { String originalName = file.getOriginalFilename(); if (!originalName.endsWith(\".jpg\") &amp;&amp; !originalName.endsWith(\".png\")) { throw new RuntimeException(\"invalid file type\"); } Path target = Paths.get(\"/var/www/html/upload/\" + originalName); file.transferTo(target); return \"/upload/\" + originalName; }这段代码看起来做了后缀校验,但实际存在多个问题:后缀判断过于简单:大小写、双后缀、特殊字符、编码差异都可能导致判断结果和服务器解析结果不一致。使用原始文件名:如果文件名包含路径分隔符、控制字符或同名文件,可能引入额外风险。上传目录在 Web 根目录:用户上传内容可以直接被访问,一旦解析配置异常,影响会放大。缺少内容校验:只看文件名无法确认文件真实类型。缺少大小限制:容易被大文件、压缩炸弹或大量小文件拖垮存储资源。安全的上传处理应该是多层校验和隔离,不依赖单个判断条件。一个更合理的处理思路包括:白名单类型、随机文件名、非 Web 目录存储、内容检测、权限控制、统一下载接口和日志审计。关键审计步骤1. 定位上传入口代码审计时可以先通过关键词定位上传逻辑,例如:MultipartFile Request.Files move_uploaded_file FormFile FileUpload transferTo SaveAs writeFile除了显式上传接口,也要关注导入、富文本编辑器、客服附件、插件市场、压缩包解压、头像裁剪等功能。这些入口经常被遗漏。2. 检查后端校验逻辑后端至少应检查以下内容:文件大小是否有限制。扩展名是否使用严格白名单。MIME 是否仅作为辅助判断,而不是唯一依据。是否读取文件头或通过可靠库识别类型。图片类文件是否经过重新编码处理。压缩包是否限制文件数量、总大小和解压路径。对于图片上传,常见做法是使用图像库重新解码再编码,丢弃原始文件中的非图像结构。示例:BufferedImage image = ImageIO.read(file.getInputStream()); if (image == null) { throw new RuntimeException(\"invalid image\"); } String safeName = UUID.randomUUID().toString() + \".jpg\"; Path target = storageDir.resolve(safeName).normalize(); ImageIO.write(image, \"jpg\", target.toFile());这类处理不能覆盖所有格式问题,但相比直接保存用户原始内容,风险会明显降低。3. 检查文件名处理不建议使用用户上传的原始文件名作为存储文件名。推荐做法是服务端生成随机名称,并单独保存原始文件名用于展示。String ext = getSafeExtension(originalName); String storedName = UUID.randomUUID().toString().replace(\"-\", \"\") + ext; Path target = storageDir.resolve(storedName).normalize(); if (!target.startsWith(storageDir)) { throw new RuntimeException(\"invalid path\"); }这里有两个细节:normalize() 之后必须判断是否仍在预期目录内,防止路径穿越。扩展名必须从白名单映射得出,不要直接信任原始后缀。4. 检查存储位置和访问方式上传文件最好存储在 Web 根目录之外,通过受控接口下载或预览。这样即使某个文件校验漏掉,也不会被 Web 容器直接解析。/data/app-upload/ ├── avatar/ ├── attachment/ └── temp/访问时由应用层读取文件并设置响应头:Content-Type: image/jpeg Content-Disposition: inline; filename=\"avatar.jpg\" X-Content-Type-Options: nosniff如果业务必须使用静态资源服务器,也应确保上传目录禁用脚本解析,只允许作为普通静态文件返回。location /uploads/ { root /data/static; autoindex off; default_type application/octet-stream; add_header X-Content-Type-Options nosniff; }不同服务器配置方式不同,核心原则是:上传目录不要具备动态执行能力。5. 检查权限与业务边界很多上传点的问题不在文件格式,而在权限边界。例如:用户 A 能否访问用户 B 上传的私有附件。低权限用户能否上传全站公告图片。临时文件是否长期公开可访问。未登录用户是否能无限制上传。上传后是否存在越权替换、删除或引用其他文件的能力。建议上传接口和文件访问接口都做权限校验,不要认为“文件名随机”就等于安全。6. 关注二次处理风险上传后常见的二次处理包括图片压缩、文档预览、压缩包解压、音视频转码、OCR 识别等。这些组件本身也可能存在安全风险。审计时可以重点关注:调用外部命令时是否拼接用户输入。处理进程是否有超时、内存和 CPU 限制。临时目录是否隔离。解析库版本是否长期未更新。失败文件是否仍被保留并可访问。如果使用命令行工具,不要把用户可控参数直接拼进字符串命令,优先使用参数数组方式调用。ProcessBuilder pb = new ProcessBuilder( \"convert\", inputPath.toString(), \"-resize\", \"800x800\", outputPath.toString() ); pb.start();推荐防护链路实际落地时,可以按下面这条链路设计:入口限制:认证、权限、频率限制、大小限制。类型白名单:基于业务只允许必要类型,不开放泛文件上传。内容识别:扩展名、MIME、文件头、解析库综合判断。安全命名:服务端生成随机文件名,原始名称仅用于展示。目录隔离:存储在非 Web 根目录,上传目录无执行权限。二次处理:图片重编码、文档转换沙箱化、压缩包安全解压。访问控制:下载和预览走统一接口,按业务权限校验。审计监控:记录上传者、文件类型、大小、访问行为和处理结果。压缩包上传的额外注意点压缩包导入功能经常被低估。除了文件类型,重点还要处理 Zip Slip、解压炸弹和大量小文件问题。Path destDir = Paths.get(\"/data/import/workdir\").toAbsolutePath().normalize(); for (ZipEntry entry : entries) { Path resolved = destDir.resolve(entry.getName()).normalize(); if (!resolved.startsWith(destDir)) { throw new RuntimeException(\"invalid zip entry path\"); } if (entry.getSize() &gt; MAX_SINGLE_FILE_SIZE) { throw new RuntimeException(\"file too large\"); } // 继续处理白名单文件类型 }还需要限制:压缩包总解压大小。文件数量。目录层级。单个文件大小。允许的文件后缀。测试与验证建议在授权测试或自测环境中,可以重点验证以下点:后端是否独立校验,不依赖前端。非法扩展名是否被拒绝。大小写后缀是否按预期处理。上传文件是否能被直接访问。上传目录是否具备脚本解析能力。文件名中特殊字符是否被安全处理。压缩包中异常路径是否被拒绝。接口是否存在未授权上传或越权访问。测试结果不要只停留在“能不能上传成功”,还要看文件最终存在哪里、以什么 Content-Type 返回、是否经过业务权限校验、失败文件是否残留。常见误区只做前端限制:前端校验只能改善体验,不能作为安全边界。只校验 MIME:MIME 可以被客户端声明,不可靠。黑名单过滤:后缀和解析规则太多,黑名单容易遗漏。随机文件名即安全:随机名不能替代类型校验和访问控制。对象存储天然安全:错误的 Bucket 权限和回源配置同样会造成风险。总结文件上传安全的关键不是某一个校验函数,而是一整条链路:入口控制、类型识别、存储隔离、执行权限收敛、访问鉴权和日志审计都要到位。从代码审计角度看,建议优先关注三个问题:文件是否直接落到 Web 可访问目录、后端是否只依赖后缀判断、访问接口是否存在权限缺失。只要这三点处理不当,上传功能的风险通常就不会低。经验上,上传功能最稳妥的设计是:用户上传的原始内容永远不要直接进入可执行环境,业务需要展示时由受控接口读取并返回。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传是 背景文 景文件 文件上 件上传 上传是 背景文件 景文件上 文件上传 件上传是 系统 统里 里很 很常 常见 见的 的功 功能 系统里 统里很 里很常 很常见 常见的 见的功 的功能 系统里很 统里很常 里很常见 很常见的 常见的功 见的功能 头像 附件 工单 单截 截图 工单截 单截图 工单截图 文档 档导 导入 入都 都会 会用 用到 文档导 档导入 导入都 入都会 都会用 会用到 文档导入 档导入都 导入都会 入都会用 都会用到 也正 正因 因为 为入 入口 口常 也正因 正因为 因为入 为入口 入口常 口常见 也正因为 正因为入 因为入口 为入口常 入口常见 数据 据不 不可 可控 数据不 据不可 不可控 数据不可 据不可控 传点 点一 一直 直是 是代 代码 码审 审计 计和 和渗 渗透 透测 测试 试中 中优 优先 先关 关注 注的 的位 位置 上传点 传点一 点一直 一直是 直是代 是代码 代码审 码审计 审计和 计和渗 和渗透 渗透测 透测试 测试中 试中优 中优先 优先关 先关注 关注的 注的位 的位置 上传点一 传点一直 点一直是 一直是代 直是代码 是代码审 代码审计 码审计和 审计和渗 计和渗透 和渗透测 渗透测试 透测试中 测试中优 试中优先 中优先关 优先关注 先关注的 关注的位 注的位置 这篇 篇文 文章 章结 结合 合我 我在 在审 计业 业务 务系 统时 时经 经常 常遇 遇到 到的 的问 问题 这篇文 篇文章 文章结 章结合 结合我 合我在 我在审 在审计 审计业 计业务 业务系 务系统 系统时 统时经 时经常 经常遇 常遇到 遇到的 到的问 的问题 这篇文章 篇文章结 文章结合 章结合我 结合我在 合我在审 我在审计 在审计业 审计业务 计业务系 业务系统 务系统时 系统时经 统时经常 时经常遇 经常遇到 常遇到的 遇到的问 到的问题 整理 理一 一套 套相 相对 对完 完整 整的 的分 分析 析思 思路 路和 和防 防护 护链 链路 整理一 理一套 一套相 套相对 相对完 对完整 完整的 整的分 的分析 分析思 析思路 思路和 路和防 和防护 防护链 护链路 整理一套 理一套相 一套相对 套相对完 相对完整 对完整的 完整的分 整的分析 的分析思 分析思路 析思路和 思路和防 路和防护 和防护链 防护链路 重点 点不 不是 是讲 重点不 点不是 不是讲 重点不是 点不是讲 如何 何利 利用 如何利 何利用 如何利用 而是 是说 说明 明为 为什 什么 么某 某些 些写 写法 法不 可靠 而是说 是说明 说明为 明为什 为什么 什么某 么某些 某些写 些写法 写法不 法不可 不可靠 而是说明 是说明为 说明为什 明为什么 为什么某 什么某些 么某些写 某些写法 些写法不 写法不可 法不可靠 以及 及在 在工 工程 程上 上如 何把 把风 风险 险压 压下 下去 以及在 及在工 在工程 工程上 程上如 上如何 如何把 何把风 把风险 风险压 险压下 压下去 以及在工 及在工程 在工程上 工程上如 程上如何 上如何把 如何把风 何把风险 把风险压 风险压下 险压下去 见问 题场 场景 景很 很多 多上 传漏 漏洞 洞并 并不 是单 单点 点失 失误 常见问 见问题 问题场 题场景 场景很 景很多 很多上 多上传 上传漏 传漏洞 漏洞并 洞并不 并不是 不是单 是单点 单点失 点失误 常见问题 见问题场 问题场景 题场景很 场景很多 景很多上 很多上传 多上传漏 上传漏洞 传漏洞并 漏洞并不 洞并不是 并不是单 不是单点 是单点失 单点失误 是多 多个 个环 环节 节同 同时 时薄 薄弱 弱造 造成 成的 而是多 是多个 多个环 个环节 环节同 节同时 同时薄 时薄弱 薄弱造 弱造成 造成的 而是多个 是多个环 多个环节 个环节同 环节同时 节同时薄 同时薄弱 时薄弱造 薄弱造成 弱造成的 见场 景包 包括 常见场 见场景 场景包 景包括 常见场景 见场景包 场景包括 仅通 通过 过前 前端 端限 限制 制文 件后 后缀 仅通过 通过前 过前端 前端限 端限制 限制文 制文件 文件后 件后缀 仅通过前 通过前端 过前端限 前端限制 端限制文 限制文件 制文件后 文件后缀 后端 端没 没有 有重 重新 新校 校验 后端没 端没有 没有重 有重新 重新校 新校验 后端没有 端没有重 没有重新 有重新校 重新校验 端只 只判 判断 断文 件名 名后 后端只 端只判 只判断 判断文 断文件 文件名 件名后 名后缀 后端只判 端只判断 只判断文 判断文件 断文件名 文件名后 件名后缀 有校 没有校 有校验 没有校验 件头
  25. 背景登录接口看起来简单,但在实际代码审计和渗透测试中,它往往是风险密度比较高的入口。认证链路通常会同时涉及账号枚举、密码校验、验证码、风控限流、会话签发、日志记录等多个环节。任何一个环节处理不当,都可能让攻击面扩大。 这篇文章整理的是我在审计 Web 系统登录模块时常用的一套检查思路,偏长期可复用,不针对某个具体产品,也不提供攻击利用流程。重点是帮助开发和安全同学识别风险点,并给出可落地的修复建议。 典型场景常见的登录接口大致如下:前端提交用户名、密码、验证码,后端校验账号状态和密码,成功后签发 Session 或 JWT。很多系统会在这个过程中增加图形验证码、短信验证码、失败次数限制、IP 限流等机制。 问题在于,很多实现只做了“功能可用”,但没有完整考虑异常路径。例如:账号不存在和密码错误返回不同提示;验证码只在前端校验;失败次数按 IP 记录但没有按账号记录;JWT 过期时间过长;登录日志里直接记录明文密码等。这些都属于认证链路里的常见缺陷。 技术分析审计登录接口时,我一般会先从数据流入手,梳理一次请求从入口到会话生成的完整路径: 请求参数:用户名、密码、验证码、设备信息、来源 IP 是否可信。账号查询:是否存在账号枚举风险,查询条件是否做了规范化处理。密码校验:是否使用安全哈希算法,是否存在明文或可逆加密存储。验证码和限流:是否在服务端校验,是否覆盖账号维度和 IP 维度。会话生成:Session ID 或 Token 是否安全随机,过期策略是否合理。日志记录:是否记录敏感字段,是否存在日志注入或泄露风险。一个比较常见的问题是返回信息过于精确。例如账号不存在返回“用户不存在”,密码错误返回“密码错误”。从用户体验看似友好,但从安全角度看,会增加账号枚举风险。更稳妥的做法是对外统一返回“账号或密码错误”,同时在服务端日志中保留更细的错误原因,便于排障。 关键检查点1. 账号枚举账号枚举不一定只来自返回文案,也可能来自响应时间、状态码、响应包大小。比如账号不存在时直接返回,账号存在时进入密码哈希校验,二者耗时明显不同。审计时可以关注这些差异是否明显。 建议: 对外错误提示保持一致。响应状态码不要根据账号是否存在做区分。必要时对不存在账号也执行等价耗时的伪校验,减少时间侧信道差异。2. 密码存储与校验密码不应明文存储,也不建议使用 MD5、SHA1 这类快速哈希直接存储。推荐使用 bcrypt、scrypt、Argon2 等适合密码存储的算法,并为每个密码使用独立盐值。 以 Spring Security 为例,使用 BCryptPasswordEncoder 是比较常见的做法: import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; PasswordEncoder encoder = new BCryptPasswordEncoder(12); // 注册或重置密码时 String hash = encoder.encode(rawPassword); // 登录校验时 boolean matched = encoder.matches(rawPassword, hash);注意不要自己拼接盐值后再做简单哈希,也不要把加密密钥和密码哈希存放在同一个配置里。密码哈希的目标是即使数据库泄露,也尽量增加离线破解成本。 3. 验证码与限流验证码不是万能手段,更不能只在前端判断。验证码校验必须在服务端完成,并且与一次性标识绑定,例如 Session、临时 Token 或服务端缓存中的 challenge id。 限流也不应只依赖 IP。现实中会遇到 NAT、代理、移动网络等场景,单纯按 IP 限制容易误伤,也可能被绕开。更稳妥的是组合多个维度: 账号维度:某账号连续失败次数。IP 维度:某 IP 单位时间内失败次数。设备或客户端指纹维度:辅助风控判断。全局维度:异常流量高峰时的整体保护。一个简单的限流策略可以参考: # 示例:Nginx 层做基础 IP 限速,应用层仍需做账号维度限制 http { limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m; server { location /api/login { limit_req zone=login_zone burst=10 nodelay; proxy_pass } } }这类配置只能作为第一层保护,不能替代业务层风控。业务层仍然需要记录失败次数、冷却时间和异常行为。 4. 会话与 Token登录成功后,系统通常会签发 Session 或 JWT。这里需要关注几个点:随机性、有效期、刷新机制、退出登录后的失效策略。 如果使用 JWT,不建议把敏感信息放进 payload。JWT 默认只是编码,不是加密。常见错误是把手机号、邮箱、角色明细、甚至内部权限字段直接塞进 Token。更稳妥的做法是只放必要的 subject、过期时间和少量上下文信息,权限仍由服务端查询或缓存控制。 JWT 示例字段应保持克制: { "sub": "user_id_12345", "iat": 1710000000, "exp": 1710003600, "iss": "example-service" }另外,退出登录要考虑服务端失效。如果系统完全依赖无状态 JWT,退出后 Token 在过期前仍可能可用。可以引入短有效期 Access Token 加 Refresh Token,或维护 Token 黑名单、版本号等机制。 5. 登录日志登录日志的价值很高,但也容易引入新的泄露风险。审计时重点看日志是否包含明文密码、验证码、完整 Token、身份证号等敏感字段。 建议记录: 用户标识:使用 userId 或脱敏后的账号。结果:成功、失败、锁定、验证码错误等内部枚举。来源:IP、User-Agent、设备标识,注意可信代理链解析。时间:精确时间戳,便于事件复盘。请求标识:traceId 或 requestId,便于串联链路。不建议记录: 明文密码、验证码、短信验证码。完整 Cookie、Authorization Header。未脱敏的手机号、邮箱、证件号。代码审计时的实用方法如果是白盒审计,可以先从路由和控制器入口定位登录相关接口,再向下追踪 Service、DAO、缓存和日志。重点关注条件分支和异常处理,因为很多认证缺陷都出现在失败路径中。 # 常见关键词检索思路,按项目语言调整 grep -R "login" ./src grep -R "password" ./src grep -R "captcha" ./src grep -R "token" ./src grep -R "BCrypt\|MD5\|SHA1" ./src看到以下模式时需要提高警惕: 直接比较明文密码:rawPassword.equals(dbPassword)。使用 MD5(password) 作为最终密码存储。验证码校验只出现在前端代码中。登录失败没有任何次数限制。异常信息直接返回给客户端。日志打印完整请求体。修复建议清单风险点建议做法账号枚举统一错误提示,减少响应差异,服务端保留详细原因弱密码存储使用 bcrypt、scrypt、Argon2 等密码哈希算法验证码绕过风险验证码必须服务端校验,并绑定一次性 challenge暴力尝试组合账号、IP、设备、全局维度做限流和冷却Token 泄露影响大缩短有效期,避免存敏感字段,支持失效机制日志泄露敏感字段脱敏,禁止记录密码和完整认证凭据注意点认证模块的安全加固要注意平衡可用性。比如账号锁定策略如果过于激进,可能被滥用造成拒绝服务;验证码如果频繁触发,会影响正常用户体验。比较合理的方式是分级处置:轻度异常增加冷却时间,中度异常触发验证码,高风险场景再进行临时锁定或二次验证。 另外,不要只在网关层做安全控制。网关、WAF、Nginx 限流都可以降低风险,但认证逻辑的最终可信判断仍应放在业务服务端。尤其是内部接口、移动端接口、老版本 API,往往会绕过部分前端或边缘层逻辑。 总结登录接口审计的核心不是找某个单点问题,而是看整个认证链路是否闭环:输入是否可信、校验是否在服务端、失败路径是否可控、会话是否可失效、日志是否可追溯且不泄露敏感信息。 实际项目里,登录模块经常随着业务迭代被不断修改,历史兼容逻辑和临时开关很容易留下风险。建议把认证链路纳入常规安全基线检查,并在新增登录方式、接入第三方认证、调整验证码策略时同步做安全评审。这样比事后补漏洞更稳,也更容易维护。 网络请求、日志与边界流量分析示意 背景 景登 登录 录接 接口 口看 看起 起来 来简 简单 背景登 景登录 登录接 录接口 接口看 口看起 看起来 起来简 来简单 背景登录 景登录接 登录接口 录接口看 接口看起 口看起来 看起来简 起来简单 但在 在实 实际 际代 代码 码审 审计 计和 和渗 渗透 透测 测试 试中 但在实 在实际 实际代 际代码 代码审 码审计 审计和 计和渗 和渗透 渗透测 透测试 测试中 但在实际 在实际代 实际代码 际代码审 代码审计 码审计和 审计和渗 计和渗透 和渗透测 渗透测试 透测试中 它往 往往 往是 是风 风险 险密 密度 度比 比较 较高 高的 的入 入口 它往往 往往是 往是风 是风险 风险密 险密度 密度比 度比较 比较高 较高的 高的入 的入口 它往往是 往往是风 往是风险 是风险密 风险密度 险密度比 密度比较 度比较高 比较高的 较高的入 高的入口 认证 证链 链路 路通 通常 常会 会同 同时 时涉 涉及 及账 账号 号枚 枚举 认证链 证链路 链路通 路通常 通常会 常会同 会同时 同时涉 时涉及 涉及账 及账号 账号枚 号枚举 认证链路 证链路通 链路通常 路通常会 通常会同 常会同时 会同时涉 同时涉及 时涉及账 涉及账号 及账号枚 账号枚举 密码 码校 校验 密码校 码校验 密码校验 验证 证码 验证码 风控 控限 限流 风控限 控限流 风控限流 会话 话签 签发 会话签 话签发 会话签发 日志 志记 记录 录等 等多 多个 个环 环节 日志记 志记录 记录等 录等多 等多个 多个环 个环节 日志记录 志记录等 记录等多 录等多个 等多个环 多个环节 任何 何一 一个 节处 处理 理不 不当 任何一 何一个 一个环 环节处 节处理 处理不 理不当 任何一个 何一个环 一个环节 个环节处 环节处理 节处理不 处理不当 都可 可能 能让 让攻 攻击 击面 面扩 扩大 都可能 可能让 能让攻 让攻击 攻击面 击面扩 面扩大 都可能让 可能让攻 能让攻击 让攻击面 攻击面扩 击面扩大 这篇 篇文 文章 章整 整理 理的 的是 是我 我在 在审 这篇文 篇文章 文章整 章整理 整理的 理的是 的是我 是我在 我在审 在审计 这篇文章 篇文章整 文章整理 章整理的 整理的是 理的是我 的是我在 是我在审 我在审计 系统 统登 录模 模块 块时 时常 常用 用的 的一 一套 套检 检查 查思 思路 系统登 统登录 登录模 录模块 模块时 块时常 时常用 常用的 用的一 的一套 一套检 套检查 检查思 查思路 系统登录 统登录模 登录模块 录模块时 模块时常 块时常用 时常用的 常用的一 用的一套 的一套检 一套检查 套检查思 检查思路 偏长 长期 期可 可复 复用 偏长期 长期可 期可复 可复用 偏长期可 长期可复 期可复用 不针 针对 对某 某个 个具 具体 体产 产品 不针对 针对某 对某个 某个具 个具体 具体产 体产品 不针对某 针对某个 对某个具 某个具体 个具体产 具体产品 也不 不提 提供 供攻 击利 利用 用流 流程 也不提 不提供 提供攻 供攻击 攻击利 击利用 利用流 用流程 也不提供 不提供攻 提供攻击 供攻击利 攻击利用 击利用流 利用流程 重点 点是 是帮 帮助 助开 开发 发和 和安 安全 全同 同学 学识 识别 别风 险点 重点是 点是帮 是帮助 帮助开 助开发 开发和 发和安 和安全 安全同 全同学 同学识 学识别 识别风 别风险 风险点 重点是帮 点是帮助 是帮助开 帮助开发 助开发和 开发和安 发和安全 和安全同 安全同学 全同学识 同学识别 学识别风 识别风险 别风险点 并给 给出 出可 可落 落地 地的 的修 修复 复建 建议 并给出 给出可 出可落 可落地 落地的 地的修 的修复 修复建 复建议 并给出可 给出可落 出可落地 可落地的 落地的修 地的修复 的修复建 修复建议 典型 型场 场景 景常 常见 见的 的登 口大 大致 致如 如下 典型场 型场景 场景常 景常见 常见的 见的登 的登录 接口大 口大致 大致如 致如下 典型场景 型场景常 场景常见 景常见的 常见的登 见的登录 的登录接 录接口大 接口大致 口大致如 大致如下 前端 端提 提交 交用 用户 户名 前端提 端提交 提交用 交用户 用户名 前端提交 端提交用 提交用户 交用户名 后端 端校 验账 号状 状态 态和 和密 后端校 端校验 校验账 验账号 账号状 号状态 状态和 态和密 和密码 后端校验 端校验账 校验账号 验账号状 账号状态 号状态和 状态和密 态和密码 成功 功后 后签 成功后 功后签 后签发 成功后签 功后签发 很多
  26. 背景文件上传功能在业务系统里很常见,比如头像、附件、工单截图、富文本图片等。它看起来只是一个普通入口,但在实际代码审计中,上传点往往是高风险区域:一旦校验不严,可能导致任意文件写入、脚本文件落地、路径穿越、存储型 XSS,甚至进一步扩大影响面。 这篇文章整理一套偏实战的排查思路,重点放在安全审计、复现验证和加固方案上。内容不讨论未授权入侵或恶意利用,只以授权测试环境为前提,帮助开发和安全同学定位问题、降低风险。 常见场景与风险点文件上传漏洞通常不是单点失误,而是多个环节叠加导致的。比较常见的问题包括: 只校验前端限制,后端没有做类型判断。仅根据文件后缀判断类型,未校验真实内容。允许用户控制文件名或保存路径,导致路径穿越或覆盖文件。上传目录位于 Web 可访问路径下,且服务端会解析其中的脚本文件。未限制文件大小、数量和压缩包解压行为,导致资源消耗风险。图片、SVG、HTML 等类型处理不当,引发存储型 XSS。实际审计时,不建议只盯着“能不能上传脚本文件”这一种结果。很多上传点即便不能执行服务端脚本,也可能通过 HTML、SVG、PDF、Office 宏、压缩包、文件名污染等方式带来风险。 代码审计入口审计上传功能时,我通常先从路由和控制器入口开始,确认上传请求最终流向哪里。常见关键字包括: upload multipart MultipartFile move_uploaded_file Request.Files FileUpload saveAs store attachment avatar import parseZip以 Java/Spring 项目为例,常见入口可能类似: @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) throws IOException { String filename = file.getOriginalFilename(); File dest = new File("/data/upload/" + filename); file.transferTo(dest); return "/upload/" + filename; }这段代码问题比较典型:原始文件名直接参与路径拼接,没有处理路径穿越;没有限制文件大小;没有校验扩展名和 MIME;没有做内容识别;返回了可访问路径。如果上传目录被 Web 服务直接暴露,风险会更高。 技术分析:不要只相信 Content-Type很多系统会通过请求头里的 Content-Type 或 Multipart 中的 MIME 来判断文件类型,例如 image/png、image/jpeg。这类字段由客户端提供,不应作为唯一依据。 更稳妥的方式是多层校验: 扩展名白名单:只允许业务确实需要的类型。服务端 MIME 检测:结合文件头特征识别,不依赖客户端传入值。文件魔数校验:检查文件头是否符合预期格式。安全解码验证:图片类文件可尝试解码重编码,避免伪装文件。存储隔离:上传文件不放在可执行目录下。例如图片上传场景,后端不应仅判断后缀为 jpg/png。可以读取文件头并使用图片库尝试解析,再重新编码保存。这样可以过滤一部分伪造内容和异常结构文件。 关键步骤:授权环境下的验证方法在授权测试环境中,可以按以下顺序验证上传点的安全性。这里重点是确认防护是否生效,而不是追求破坏性结果。 1. 确认上传目录和访问方式先判断上传后的文件是否能通过 URL 直接访问。如果上传目录与应用静态资源目录混在一起,就需要格外关注。 # 示例:检查响应中返回的访问路径 POST /api/upload HTTP/1.1 Content-Type: multipart/form-data; boundary=----test ------test Content-Disposition: form-data; name="file"; filename="test.png" Content-Type: image/png ...file content... ------test--关注响应中是否返回类似 /upload/test.png、/static/upload/test.png 这样的路径。如果返回的是对象存储临时地址,也要确认是否设置了正确的 Content-Type 和下载策略。 2. 测试扩展名白名单上传功能应采用白名单策略,而不是黑名单。黑名单容易漏掉大小写、双后缀、特殊解析规则等情况。 允许示例:jpg、jpeg、png、gif、pdf 拒绝示例:php、jsp、jspx、asp、aspx、html、svg、shtml、exe、sh、bat需要注意,svg 虽然是图片格式,但其本质是 XML,可能包含脚本或外部引用。如果业务没有强需求,不建议允许直接上传 svg;如果必须支持,应进行严格清洗并设置下载而非内联展示。 3. 检查文件名处理用户上传的原始文件名不应直接落盘。常见风险包括路径穿越、覆盖已有文件、特殊字符污染日志或响应头。 // 不推荐 String filename = file.getOriginalFilename(); Path dest = Paths.get(uploadDir, filename); // 推荐思路 String ext = getSafeExtension(file); String filename = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path dest = Paths.get(uploadDir).resolve(filename).normalize(); if (!dest.startsWith(Paths.get(uploadDir).normalize())) { throw new SecurityException("invalid path"); }实际项目中,建议将原始文件名只作为元数据保存,并在展示时做 HTML 转义,不参与真实存储路径。 4. 检查大小、数量和解压逻辑上传接口如果没有限制大小和频率,容易被用来消耗磁盘和带宽。压缩包上传则要关注 Zip Slip 和解压炸弹风险。 # Nginx 限制请求体大小示例 server { client_max_body_size 10m; }# Spring Boot 配置示例 spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=20MB如果业务支持压缩包导入,解压时必须检查每个条目的规范化路径,确保不会写出目标目录。 Path targetDir = Paths.get("/data/import").normalize(); Path output = targetDir.resolve(entry.getName()).normalize(); if (!output.startsWith(targetDir)) { throw new SecurityException("zip entry path traversal detected"); }5. 检查响应头与展示方式上传后的文件如果可以在线预览,需要关注浏览器解析行为。对于不需要内联展示的文件,建议使用附件下载,并设置合适的响应头。 Content-Disposition: attachment; filename="download.pdf" X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none'; img-src 'self'; style-src 'self'尤其是用户可控内容,如果被浏览器当作 HTML/SVG 解析,就可能引入存储型 XSS。对象存储场景也一样,不能只把文件丢到桶里,还要检查元数据里的 Content-Type。 加固建议文件上传防护建议分层做,不要依赖单一校验点。 使用白名单:只允许业务明确需要的后缀和类型。服务端强校验:后端根据文件内容识别类型,不信任前端参数。随机文件名:使用 UUID、雪花 ID 等生成存储名,避免用户控制路径。目录隔离:上传目录与应用代码目录分离,禁止脚本执行。权限最小化:上传目录只授予必要读写权限,不给执行权限。大小限制:限制单文件大小、总请求大小、上传频率。内容处理:图片类文件可重编码;文档类文件可转存为安全格式或走下载。安全响应头:设置 nosniff、Content-Disposition、CSP 等。日志审计:记录上传用户、文件哈希、大小、类型、来源 IP、处理结果。异步扫描:对高风险文件接入杀毒或内容安全扫描,但不要把扫描当作唯一防线。参考的安全实现流程一个相对稳妥的上传处理流程可以这样设计: 1. 验证登录态和业务权限 2. 检查请求大小和文件数量 3. 获取原始文件名,仅用于展示元数据 4. 解析扩展名,匹配白名单 5. 读取文件头,做内容类型识别 6. 对图片等格式进行解码与重编码 7. 生成随机文件名,保存到非 Web 可执行目录 8. 保存文件元数据与哈希 9. 返回文件 ID,不直接暴露真实路径 10. 下载时通过鉴权接口读取文件并设置安全响应头如果业务允许公开访问文件,也建议通过独立域名或对象存储隔离,避免与主站共享 Cookie 和执行上下文。 审计时容易忽略的点检查项风险建议SVG 上传可能触发脚本或外部资源加载默认禁止,必须支持时做清洗并限制展示方式原始文件名回显可能造成 XSS 或日志污染展示前 HTML 转义,存储名随机化压缩包解压路径穿越、解压炸弹检查规范化路径、限制层级和总大小对象存储元数据错误 Content-Type 导致浏览器解析设置固定类型和下载策略临时目录临时文件残留或权限过宽定期清理,限制权限注意点上传漏洞的验证应始终在授权范围内进行。测试重点应放在确认校验链路是否可靠、是否能隔离风险,而不是尝试造成破坏性影响。另外,安全扫描工具可以辅助发现问题,但不适合完全替代人工审计。上传逻辑经常与业务强相关,例如“普通用户只能上传图片,管理员可以上传模板文件”,这种差异需要结合权限模型一起看。 总结文件上传安全的核心不是某一个判断条件,而是完整链路的风险控制:类型校验、路径处理、存储隔离、访问控制、响应头、日志审计都要覆盖。比较理想的状态是,即便某一层校验出现遗漏,文件也不会进入可执行环境,用户也无法通过上传内容影响主站上下文。 在代码审计中,建议把上传点作为高优先级入口处理。只要发现“原始文件名直接保存”“上传目录可直接访问”“只依赖 Content-Type”“压缩包未检查路径”这几类问题,就值得进一步复核并推动修复。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传功 功能 能在 在业 业务 务系 系统 统里 里很 很常 常见 背景文 景文件 文件上 件上传 上传功 传功能 功能在 能在业 在业务 业务系 务系统 系统里 统里很 里很常 很常见 背景文件 景文件上 文件上传 件上传功 上传功能 传功能在 功能在业 能在业务 在业务系 业务系统 务系统里 系统里很 统里很常 里很常见 比如 如头 头像 比如头 如头像 比如头像 附件 工单 单截 截图 工单截 单截图 工单截图 富文 文本 本图 图片 片等 富文本 文本图 本图片 图片等 富文本图 文本图片 本图片等 它看 看起 起来 来只 只是 是一 一个 个普 普通 通入 入口 它看起 看起来 起来只 来只是 只是一 是一个 一个普 个普通 普通入 通入口 它看起来 看起来只 起来只是 来只是一 只是一个 是一个普 一个普通 个普通入 普通入口 但在 在实 实际 际代 代码 码审 审计 计中 但在实 在实际 实际代 际代码 代码审 码审计 审计中 但在实际 在实际代 实际代码 际代码审 代码审计 码审计中 传点 点往 往往 往是 是高 高风 风险 险区 区域 上传点 传点往 点往往 往往是 往是高 是高风 高风险 风险区 险区域 上传点往 传点往往 点往往是 往往是高 往是高风 是高风险 高风险区 风险区域 一旦 旦校 校验 验不 不严 一旦校 旦校验 校验不 验不严 一旦校验 旦校验不 校验不严 可能 能导 导致 致任 任意 意文 件写 写入 可能导 能导致 导致任 致任意 任意文 意文件 文件写 件写入 可能导致 能导致任 导致任意 致任意文 任意文件 意文件写 文件写入 脚本 本文 件落 落地 脚本文 本文件 文件落 件落地 脚本文件 本文件落 文件落地 路径 径穿 穿越 路径穿 径穿越 路径穿越 存储 储型 存储型 甚至 至进 进一 一步 步扩 扩大 大影 影响 响面 甚至进 至进一 进一步 一步扩 步扩大 扩大影 大影响 影响面 甚至进一 至进一步 进一步扩 一步扩大 步扩大影 扩大影响 大影响面 这篇 篇文 文章 章整 整理 理一 一套 套偏 偏实 实战 战的 的排 排查 查思 思路 这篇文 篇文章 文章整 章整理 整理一 理一套 一套偏 套偏实 偏实战 实战的 战的排 的排查 排查思 查思路 这篇文章 篇文章整 文章整理 章整理一 整理一套 理一套偏 一套偏实 套偏实战 偏实战的 实战的排 战的排查 的排查思 排查思路 重点 点放 放在 在安 安全 全审 重点放 点放在 放在安 在安全 安全审 全审计 重点放在 点放在安 放在安全 在安全审 安全审计 复现 现验 验证 证和 和加 加固 固方 方案 案上 复现验 现验证 验证和 证和加 和加固 加固方 固方案 方案上 复现验证 现验证和 验证和加 证和加固 和加固方 加固方案 固方案上 内容 容不 不讨 讨论 论未 未授 授权 权入 入侵 侵或 或恶 恶意 意利 利用 内容不 容不讨 不讨论 讨论未 论未授 未授权 授权入 权入侵 入侵或 侵或恶 或恶意 恶意利 意利用 内容不讨 容不讨论 不讨论未 讨论未授 论未授权 未授权入 授权入侵 权入侵或 入侵或恶 侵或恶意 或恶意利 恶意利用 只以 以授 权测 测试 试环 环境 境为 为前 前提 只以授 以授权 授权测 权测试 测试环 试环境 环境为 境为前 为前提 只以授权 以授权测 授权测试 权测试环 测试环境 试环境为 环境为前 境为前提 帮助 助开 开发 发和 和安 全同 同学 学定 定位 位问 问题 帮助开 助开发 开发和 发和安 和安全 安全同 全同学 同学定 学定位 定位问 位问题 帮助开发 助开发和 开发和安 发和安全 和安全同 安全同学 全同学定 同学定位 学定位问 定位问题 降低 低风 降低风 低风险 降低风险 见场 场景 景与 与风 险点 点文 传漏 漏洞 洞通 通常 常不 不是 是单 单点 点失 失误 常见场 见场景 场景与 景与风 与风险 风险点 险点文 点文件 上传漏 传漏洞 漏洞通 洞通常 通常不 常不是 不是单 是单点 单点失 点失误 常见场景 见场景与 场景与风 景与风险 与风险点 风险点文 险点文件 点文件上 件上传漏 上传漏洞 传漏洞通 漏洞通常 洞通常不 通常不是 常不是单 不是单点 是单点失 单点失误 而是 是多 多个 个环 环节 节叠 叠加 加导 致的 而是多 是多个 多个环 个环节 环节叠 节叠加 叠加导 加导致 导致的 而是多个 是多个环 多个环节 个环节叠 环节叠加 节叠加导 叠加导致 加导致的 比较 较常 见的 的问 题包 包括 比较常 较常见 常见的 见的问 的问题
  27. 背景文件上传一直是 Java Web 项目里比较容易被低估的风险点。很多业务只是做了前端后缀限制,或者在服务端简单判断文件名是否以 .jpg、.png 结尾,但实际落地到生产环境时,还会受到解析容器、静态资源目录、对象存储回源、Nginx 配置、权限控制等因素影响。 这篇文章整理一次比较典型的 Java Web 文件上传代码审计过程,重点放在如何识别风险、如何构造安全的验证链路,以及上线修复时容易遗漏的细节。内容偏防御和审计,不涉及利用链扩展。 问题场景某个后台系统提供了素材上传功能,支持上传头像、活动图片和附件。审计时看到上传接口大致流程如下: @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) throws IOException { String fileName = file.getOriginalFilename(); if (!fileName.endsWith(".jpg") && !fileName.endsWith(".png")) { throw new RuntimeException("invalid file type"); } File dest = new File("/data/app/static/upload/" + fileName); file.transferTo(dest); return "/upload/" + fileName; }这段代码看起来做了后缀检查,但存在几个常见问题: 直接信任 getOriginalFilename(),存在路径穿越和特殊文件名风险。只判断后缀,没有校验真实文件类型。文件名由用户控制,可能覆盖已有文件。上传目录在静态资源目录下,文件可被直接访问。没有大小限制、权限隔离和异常日志审计。技术分析文件上传安全不能只看“能不能传上来”,还要看上传后的文件会被什么组件处理。风险通常来自以下几个环节。 1. 文件名处理风险用户提交的原始文件名不应该直接参与服务端路径拼接。除了常见的 ../,还需要关注 URL 编码、反斜杠、Unicode 混淆、空白字符、超长文件名等情况。即使业务只允许图片,也建议服务端完全丢弃用户文件名,改用随机文件名。 String original = file.getOriginalFilename(); String ext = FilenameUtils.getExtension(original).toLowerCase(Locale.ROOT); String safeName = UUID.randomUUID().toString().replace("-", "") + "." + ext;注意:这里提取扩展名只是为了保留展示或下载体验,不能把它当作唯一安全判断。 2. 后缀校验不足单纯的 endsWith() 有很多问题,例如大小写差异、双后缀、尾部空白、特殊字符等。更稳妥的做法是使用白名单,并且在标准化后判断。 private static final Set<String> ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "gif", "pdf"); String ext = FilenameUtils.getExtension(originalName); if (ext == null) { throw new IllegalArgumentException("missing extension"); } ext = ext.toLowerCase(Locale.ROOT).trim(); if (!ALLOWED_EXT.contains(ext)) { throw new IllegalArgumentException("unsupported file type"); }如果业务并不需要支持附件,建议只保留必要类型,上传类型越少,后续处理面越小。 3. MIME 与文件头校验MultipartFile.getContentType() 来自客户端请求头,不能完全信任。实践中可以结合文件头魔数和服务端解析库进行检测。比如图片类文件,可以用 ImageIO 读取并确认宽高;文档类文件可以使用 Apache Tika 做基础识别。 BufferedImage image = ImageIO.read(file.getInputStream()); if (image == null) { throw new IllegalArgumentException("invalid image content"); } int width = image.getWidth(); int height = image.getHeight(); if (width <= 0 || height <= 0 || width > 8000 || height > 8000) { throw new IllegalArgumentException("invalid image size"); }这里不建议只检查前几个字节后就放行。文件头校验能拦截一部分低成本伪造,但不是完整的内容安全方案。 4. 上传目录与执行权限上传目录不要放在应用可执行路径下,也不要让脚本解释器处理上传文件。比较稳妥的设计是: 上传文件存储到应用目录之外,例如 /data/uploads。应用以最小权限用户运行,只授予必要读写权限。下载通过受控接口或独立静态服务输出。Web 服务器对上传目录禁用脚本解析。Nginx 层可以增加一些基本限制,例如: location /uploads/ { alias /data/uploads/; autoindex off; default_type application/octet-stream; location ~* \.(php|jsp|jspx|asp|aspx|sh|py|pl|cgi)$ { return 403; } }如果使用对象存储,也要关注 Bucket 权限、回源域名、Content-Type、Content-Disposition,以及是否允许用户控制元数据。 5. 覆盖与并发问题直接使用原文件名保存,除了路径风险,还可能造成覆盖。攻击面不一定只是代码执行,也可能是业务数据污染,例如替换某个公开素材或诱导管理员查看异常文件。建议服务端生成不可预测文件名,并且按日期或业务维度分目录。 LocalDate now = LocalDate.now(); Path baseDir = Paths.get("/data/uploads"); Path targetDir = baseDir.resolve(now.toString()); Files.createDirectories(targetDir); String safeName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path target = targetDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("invalid upload path"); } try (InputStream in = file.getInputStream()) { Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); }如果不希望覆盖,可以使用 StandardOpenOption.CREATE_NEW 或先判断文件是否存在。实际项目里 UUID 冲突概率很低,但编码习惯上仍建议明确策略。 关键修复步骤针对上述问题,可以把上传接口整理成一条相对完整的校验链路: 限制上传大小:在框架和反向代理层同时设置限制。丢弃用户原始文件名:只保留扩展名或记录到数据库展示字段。扩展名白名单:标准化后判断,不使用黑名单。内容校验:结合文件头、解析库、业务规则判断。目录隔离:上传目录不放到应用执行路径。权限控制:文件读写权限最小化,下载接口做鉴权。日志记录:记录上传用户、文件大小、检测结果、存储路径映射。Spring Boot 中可以先配置上传大小: spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=10MBNginx 层也应同步限制,避免大文件请求直接打到后端: client_max_body_size 10m;参考实现片段下面是一段简化后的防御性写法,适合作为审计时的对照,不建议直接复制到生产环境而不做业务适配。 @PostMapping("/upload/image") public UploadResult uploadImage(@RequestParam("file") MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("empty file"); } if (file.getSize() > 5 * 1024 * 1024) { throw new IllegalArgumentException("file too large"); } String originalName = Optional.ofNullable(file.getOriginalFilename()).orElse(""); String ext = FilenameUtils.getExtension(originalName).toLowerCase(Locale.ROOT).trim(); Set<String> allowed = Set.of("jpg", "jpeg", "png"); if (!allowed.contains(ext)) { throw new IllegalArgumentException("unsupported extension"); } BufferedImage image = ImageIO.read(file.getInputStream()); if (image == null) { throw new IllegalArgumentException("invalid image"); } if (image.getWidth() > 8000 || image.getHeight() > 8000) { throw new IllegalArgumentException("image dimension too large"); } Path baseDir = Paths.get("/data/uploads/images").toAbsolutePath().normalize(); Path dayDir = baseDir.resolve(LocalDate.now().toString()).normalize(); Files.createDirectories(dayDir); String safeName = UUID.randomUUID().toString().replace("-", "") + "." + ext; Path target = dayDir.resolve(safeName).normalize(); if (!target.startsWith(baseDir)) { throw new SecurityException("invalid path"); } try (InputStream in = file.getInputStream()) { Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); } return new UploadResult(safeName, file.getSize(), image.getWidth(), image.getHeight()); }审计时的检查清单检查点风险表现建议文件名直接拼接原始文件名服务端生成随机文件名后缀黑名单或 endsWith 判断标准化后白名单判断内容只信任 Content-Type结合解析库和业务规则路径上传到 Web 根目录存储目录与执行目录隔离权限上传文件可被解释执行禁用脚本解析,最小权限大小无限制上传代理层和应用层同时限制访问所有上传文件公开访问敏感附件走鉴权下载注意点图片重编码是一种有效的降风险手段。对于头像、封面图这类场景,可以读取后重新编码输出,避免保留原始复杂内容。不要把文件安全完全寄托在杀毒引擎上。杀毒可以作为补充检测,但上传链路本身仍要做好类型、权限和隔离。如果业务允许压缩包上传,需要额外检查解压路径、压缩炸弹、文件数量、总大小和嵌套层级。日志不要记录完整敏感路径给前端,也不要把服务端绝对路径暴露在接口返回中。修复后要补充回归测试,尤其是大小写后缀、空文件、超大图片、异常文件名、并发上传等用例。总结文件上传漏洞的本质不是某一个判断条件缺失,而是“用户可控内容进入服务端文件系统后,被后续组件以危险方式处理”。审计时不要只停留在后缀判断,要沿着上传、存储、访问、解析、权限这条链路完整看一遍。 比较稳妥的修复思路是:用户文件名不可信、扩展名只做辅助、内容必须校验、目录必须隔离、权限必须收敛、访问必须受控。把这些基础点落实好,绝大多数常见上传风险都能被压到可控范围内。 代码审计、调用链与关键函数定位示意 背景 景文 文件 件上 上传 传一 一直 直是 背景文 景文件 文件上 件上传 上传一 传一直 一直是 背景文件 景文件上 文件上传 件上传一 上传一直 传一直是 项目 目里 里比 比较 较容 容易 易被 被低 低估 估的 的风 风险 险点 项目里 目里比 里比较 比较容 较容易 容易被 易被低 被低估 低估的 估的风 的风险 风险点 项目里比 目里比较 里比较容 比较容易 较容易被 容易被低 易被低估 被低估的 低估的风 估的风险 的风险点 很多 多业 业务 务只 只是 是做 做了 了前 前端 端后 后缀 缀限 限制 很多业 多业务 业务只 务只是 只是做 是做了 做了前 了前端 前端后 端后缀 后缀限 缀限制 很多业务 多业务只 业务只是 务只是做 只是做了 是做了前 做了前端 了前端后 前端后缀 端后缀限 后缀限制 或者 者在 在服 服务 务端 端简 简单 单判 判断 断文 件名 名是 是否 否以 或者在 者在服 在服务 服务端 务端简 端简单 简单判 单判断 判断文 断文件 文件名 件名是 名是否 是否以 或者在服 者在服务 在服务端 服务端简 务端简单 端简单判 简单判断 单判断文 判断文件 断文件名 文件名是 件名是否 名是否以 结尾 但实 实际 际落 落地 地到 到生 生产 产环 环境 境时 但实际 实际落 际落地 落地到 地到生 到生产 生产环 产环境 环境时 但实际落 实际落地 际落地到 落地到生 地到生产 到生产环 生产环境 产环境时 还会 会受 受到 到解 解析 析容 容器 还会受 会受到 受到解 到解析 解析容 析容器 还会受到 会受到解 受到解析 到解析容 解析容器 静态 态资 资源 源目 目录 静态资 态资源 资源目 源目录 静态资源 态资源目 资源目录 对象 象存 存储 储回 回源 对象存 象存储 存储回 储回源 对象存储 象存储回 存储回源 配置 权限 限控 控制 制等 等因 因素 素影 影响 权限控 限控制 控制等 制等因 等因素 因素影 素影响 权限控制 限控制等 控制等因 制等因素 等因素影 因素影响 这篇 篇文 文章 章整 整理 理一 一次 次比 较典 典型 型的 这篇文 篇文章 文章整 章整理 整理一 理一次 一次比 次比较 比较典 较典型 典型的 这篇文章 篇文章整 文章整理 章整理一 整理一次 理一次比 一次比较 次比较典 比较典型 较典型的 传代 代码 码审 审计 计过 过程 上传代 传代码 代码审 码审计 审计过 计过程 件上传代 上传代码 传代码审 代码审计 码审计过 审计过程 重点 点放 放在 在如 如何 何识 识别 别风 重点放 点放在 放在如 在如何 如何识 何识别 识别风 别风险 重点放在 点放在如 放在如何 在如何识 如何识别 何识别风 识别风险 何构 构造 造安 安全 全的 的验 验证 证链 链路 如何构 何构造 构造安 造安全 安全的 全的验 的验证 验证链 证链路 如何构造 何构造安 构造安全 造安全的 安全的验 全的验证 的验证链 验证链路 以及 及上 上线 线修 修复 复时 时容 易遗 遗漏 漏的 的细 细节 以及上 及上线 上线修 线修复 修复时 复时容 时容易 容易遗 易遗漏 遗漏的 漏的细 的细节 以及上线 及上线修 上线修复 线修复时 修复时容 复时容易 时容易遗 容易遗漏 易遗漏的 遗漏的细 漏的细节 内容 容偏 偏防 防御 御和 和审 内容偏 容偏防 偏防御 防御和 御和审 和审计 内容偏防 容偏防御 偏防御和 防御和审 御和审计 不涉 涉及 及利 利用 用链 链扩 扩展 不涉及 涉及利 及利用 利用链 用链扩 链扩展 不涉及利 涉及利用 及利用链 利用链扩 用链扩展 问题 题场 场景 景某 某个 个后 后台 台系 系统 统提 提供 供了 了素 素材 材上 传功 功能 问题场 题场景 场景某 景某个 某个后 个后台 后台系 台系统 系统提 统提供 提供了 供了素 了素材 素材上 材上传 上传功 传功能 问题场景 题场景某 场景某个 景某个后 某个后台 个后台系 后台系统 台系统提 系统提供 统提供了 提供了素 供了素材 了素材上 素材上传 材上传功 上传功能 支持 持上 传头 头像 支持上 持上传 上传头 传头像 支持上传 持上传头 上传头像 活动 动图 图片 片和 和附 附件 活动图 动图片 图片和 片和附 和附件 活动图片 动图片和 图片和附 片和附件 计时 时看 看到 到上 传接 接口 口大 大致 致流 流程 程如 如下 审计时 计时看 时看到 看到上 到上传 上传接 传接口 接口大 口大致 大致流 致流程 流程如 程如下 审计时看 计时看到 时看到上 看到上传
  28. 背景前段时间协助一个内部业务系统做安全排查,系统本身不大:前端是常见管理后台,后端是 Java Spring Boot,数据库为 MySQL,部署在内网 Kubernetes 环境中。问题来源是业务侧发现某个查询接口在异常参数下响应明显变慢,同时日志里出现了几条比较奇怪的 SQL 报错。 这类场景在内网系统里很常见:业务认为“只在内网访问”风险不高,开发为了赶进度也容易把参数校验、权限边界、日志脱敏放得比较松。实际排查下来,问题不止一个 SQL 注入点,还牵出了一些接口权限和审计日志上的薄弱点。这里把排查过程整理成一篇长期可复用的代码审计思路,不涉及攻击利用,只讨论如何发现、验证和修复。 问题场景业务接口大致是一个列表查询功能,支持按用户、部门、状态、时间范围等条件过滤。接口路径类似: GET /api/order/list?deptId=12&status=paid&sort=create_time&order=desc日志里出现的异常主要集中在排序参数和部分条件参数上。安全排查时比较关注三类问题: 动态 SQL 拼接是否可控,特别是排序字段、排序方向、模糊查询条件。接口是否只做了登录校验,没有做数据权限校验。异常日志是否暴露 SQL、表名、字段名或敏感业务数据。技术分析先看一个简化后的 Mapper 写法,这类代码在后台系统里非常常见: <select id="listOrders" resultType="OrderVO"> SELECT id, order_no, user_id, dept_id, amount, status, create_time FROM biz_order WHERE deleted = 0 <if test="deptId != null"> AND dept_id = #{deptId} </if> <if test="status != null and status != ''"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND order_no LIKE CONCAT('%', #{keyword}, '%') </if> ORDER BY ${sort} ${order} </select>这里条件参数使用 #{} 绑定,问题不大;真正危险的是 ${sort} 和 ${order}。在 MyBatis 中,#{} 会走预编译占位符,${} 是直接字符串替换。排序字段无法用普通占位符绑定,所以很多项目会直接拼进去,如果没有白名单,就会形成风险点。 需要注意的是,代码审计时不能只搜索 ${。一些项目会把 SQL 拼接放在 Service 层或自定义 QueryBuilder 里,例如: String sql = "select * from biz_order where deleted = 0"; if (StringUtils.hasText(req.getStatus())) { sql += " and status = '" + req.getStatus() + "'"; } if (StringUtils.hasText(req.getSort())) { sql += " order by " + req.getSort() + " " + req.getOrder(); }这种写法更隐蔽,单靠 Mapper XML 搜索不一定能覆盖。建议同时检索以下关键词: ${ statement.executeQuery createNativeQuery order by sort order append( StringBuilder @Query关键排查步骤1. 从接口参数流向开始看先定位 Controller 入参对象,例如: @GetMapping("/list") public PageResult<OrderVO> list(OrderQueryReq req) { return orderService.list(req); }检查 OrderQueryReq 中哪些字段会进入数据库查询。重点关注这些字段: sort、order、orderBy、field:常用于排序。keyword、name、code:常用于模糊查询。ids、deptIds:常用于 IN 查询,可能存在字符串拼接。startTime、endTime:关注类型是否为字符串,以及是否直接拼接。如果请求对象没有校验注解,也没有在 Service 层做白名单转换,基本就要继续往下追。 2. 区分可参数化与不可参数化位置SQL 中大部分值都可以参数化,例如 status、deptId、keyword。但字段名、表名、排序方向这类结构性位置不能直接用占位符解决,所以必须做白名单映射。 比较稳妥的做法是前端传入逻辑字段,后端映射为真实字段: private static final Map<String, String> SORT_FIELD_MAP = Map.of( "createTime", "create_time", "amount", "amount", "status", "status" ); public String safeSortField(String sort) { return SORT_FIELD_MAP.getOrDefault(sort, "create_time"); } public String safeOrder(String order) { if ("asc".equalsIgnoreCase(order)) { return "ASC"; } return "DESC"; }Mapper 里即便仍然使用 ${},也只允许进入白名单处理后的值: ORDER BY ${safeSort} ${safeOrder}这里的关键点不是“用了 ${} 就一定有漏洞”,而是“进入 ${} 的内容是否完全由服务端白名单生成”。 3. 检查数据权限,不只看登录态这次排查中另一个问题是:接口要求登录,但没有限制部门数据范围。也就是说,用户只要修改 deptId,就可能查询到不属于自己部门的数据。这个问题在内网管理后台里比 SQL 注入更常见。 建议审计时看三层: Controller 是否只校验了登录和菜单权限。Service 是否根据当前用户上下文补充数据范围。SQL 是否最终带上了用户可访问部门、租户、组织等限制条件。一个相对清晰的处理方式是,不直接信任请求中的 deptId,而是与当前用户可访问范围取交集: Set<Long> allowedDeptIds = authContext.getAllowedDeptIds(); Set<Long> queryDeptIds = normalize(req.getDeptIds()); Set<Long> finalDeptIds = queryDeptIds.isEmpty() ? allowedDeptIds : intersection(queryDeptIds, allowedDeptIds); if (finalDeptIds.isEmpty()) { return PageResult.empty(); } req.setFinalDeptIds(finalDeptIds);这类逻辑最好沉到统一的数据权限组件里,不建议每个业务接口自己写一遍,否则后续很难保证一致性。 4. 日志与异常处理排查时发现异常日志会把完整 SQL 和部分请求参数直接打印出来。对开发排障确实方便,但在生产环境容易泄露表结构、字段名和业务数据。 建议做几件事: 生产环境关闭 SQL 明文打印,必要时只记录 SQL 模板和 traceId。请求日志对手机号、身份证号、邮箱、订单号等字段做脱敏。接口响应不返回数据库异常原文,只返回统一错误码。保留服务端详细日志,但限制访问权限和留存周期。统一异常返回可以类似这样: @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ApiResult<Void> handle(Exception e) { log.error("request failed, traceId={}", TraceContext.traceId(), e); return ApiResult.fail("SYSTEM_ERROR", "系统繁忙,请稍后再试"); } }修复建议结合这次排查,给出一组比较实用的修复清单: 所有查询值使用预编译参数绑定,避免字符串拼接。排序字段、排序方向、动态表名等结构性参数必须使用服务端白名单。请求 DTO 增加基础格式校验,例如长度、枚举、时间范围。数据权限在服务端重新计算,不信任前端传入的组织、部门、租户字段。统一异常处理,避免把 SQL 错误、堆栈、表字段返回给前端。针对高风险接口补充审计日志,记录用户、时间、资源范围和操作结果。为关键查询增加单元测试或集成测试,覆盖非法排序字段、越权部门、超长 keyword 等场景。可复用的审计检查表检查项关注点建议动态 SQL${}、字符串拼接、Native Query值参数化,结构参数白名单排序参数sort、order、orderBy逻辑字段映射真实字段模糊查询keyword 长度、特殊字符、性能限制长度,必要时增加索引或搜索服务数据权限部门、租户、组织范围与当前用户授权范围取交集异常响应SQL 报错、堆栈、表结构统一错误码,服务端留详细日志日志记录敏感字段、完整 SQL脱敏、降级、控制访问权限注意点有几个细节容易被忽略: 不要只看 Controller 上有没有权限注解,权限注解通常解决的是“能不能进接口”,不一定解决“能看哪些数据”。不要把前端下拉框当成安全边界,任何请求参数都可能被手工修改。不要认为内网系统就不需要防护,内网账号泄露、越权访问、测试环境暴露都很常见。不要过度依赖 WAF 或网关规则,代码层面的白名单和权限校验才是根本。不要在修复时简单过滤关键字,这类方式容易误伤业务,也很难覆盖完整语法。实际审计时,我更倾向于从“参数进入哪里、是否改变 SQL 结构、是否突破数据边界”这三个问题切入。比单纯扫关键字更慢一点,但更容易发现真实风险。总结这次问题表面上是一个查询接口异常,深入看其实包含了动态 SQL、数据权限和日志暴露三个方面。对后台系统来说,列表查询接口数量多、参数复杂,是代码审计里非常值得优先看的区域。 比较可靠的治理方式不是临时加过滤,而是形成固定模式:查询值参数化,结构参数白名单,数据范围服务端计算,异常和日志统一收口。只要这几条落地,很多常见 Web 风险都能在编码阶段被挡住,后续维护成本也会低很多。 代码审计、调用链与关键函数定位示意 背景 景前 前段 段时 时间 间协 协助 助一 一个 个内 内部 部业 业务 务系 系统 统做 做安 安全 全排 排查 背景前 景前段 前段时 段时间 时间协 间协助 协助一 助一个 一个内 个内部 内部业 部业务 业务系 务系统 系统做 统做安 做安全 安全排 全排查 背景前段 景前段时 前段时间 段时间协 时间协助 间协助一 协助一个 助一个内 一个内部 个内部业 内部业务 部业务系 业务系统 务系统做 系统做安 统做安全 做安全排 安全排查 统本 本身 身不 不大 系统本 统本身 本身不 身不大 系统本身 统本身不 本身不大 前端 端是 是常 常见 见管 管理 理后 后台 前端是 端是常 是常见 常见管 见管理 管理后 理后台 前端是常 端是常见 是常见管 常见管理 见管理后 管理后台 后端 后端是 数据 据库 库为 数据库 据库为 数据库为 部署 署在 在内 内网 部署在 署在内 在内网 部署在内 署在内网 环境 境中 环境中 问题 题来 来源 源是 是业 务侧 侧发 发现 现某 某个 个查 查询 询接 接口 口在 在异 异常 常参 参数 数下 下响 响应 应明 明显 显变 变慢 问题来 题来源 来源是 源是业 是业务 业务侧 务侧发 侧发现 发现某 现某个 某个查 个查询 查询接 询接口 接口在 口在异 在异常 异常参 常参数 参数下 数下响 下响应 响应明 应明显 明显变 显变慢 问题来源 题来源是 来源是业 源是业务 是业务侧 业务侧发 务侧发现 侧发现某 发现某个 现某个查 某个查询 个查询接 查询接口 询接口在 接口在异 口在异常 在异常参 异常参数 常参数下 参数下响 数下响应 下响应明 响应明显 应明显变 明显变慢 同时 时日 日志 志里 里出 出现 现了 了几 几条 条比 比较 较奇 奇怪 怪的 同时日 时日志 日志里 志里出 里出现 出现了 现了几 了几条 几条比 条比较 比较奇 较奇怪 奇怪的 同时日志 时日志里 日志里出 志里出现 里出现了 出现了几 现了几条 了几条比 几条比较 条比较奇 比较奇怪 较奇怪的 报错 这类 类场 场景 景在 网系 统里 里很 很常 这类场 类场景 场景在 景在内 内网系 网系统 系统里 统里很 里很常 很常见 这类场景 类场景在 场景在内 景在内网 在内网系 内网系统 网系统里 系统里很 统里很常 里很常见 务认 认为 业务认 务认为 业务认为 只在 网访 访问 只在内 内网访 网访问 只在内网 在内网访 内网访问 风险 险不 不高 风险不 险不高 风险不高 开发 发为 为了 了赶 赶进 进度 度也 也容 容易 易把 把参 数校 校验 开发为 发为了 为了赶 了赶进 赶进度 进度也 度也容 也容易 容易把 易把参 把参数 参数校 数校验 开发为了 发为了赶 为了赶进 了赶进度 赶进度也 进度也容 度也容易 也容易把 容易把参 易把参数 把参数校 参数校验 权限 限边 边界 权限边 限边界 权限边界 志脱 脱敏 敏放 放得 得比 较松 日志脱 志脱敏 脱敏放 敏放得 放得比 得比较 比较松 日志脱敏 志脱敏放 脱敏放得 敏放得比 放得比较 得比较松 实际 际排 查下 下来 实际排 际排查 排查下 查下来 实际排查 际排查下 排查下来 题不 不止 止一 问题不 题不止 不止一 止一个 问题不止 题不止一 不止一个 注入 入点 注入点 还牵 牵出 出了 了一 一些 些接 口权 限和 和审 审计 计日 志上 上的 的薄 薄弱 弱点 还牵出 牵出了 出了一 了一些 一些接 些接口 接口权 口权限 权限和 限和审 和审计 审计日 计日志 日志上 志上的 上的薄 的薄弱 薄弱点 还牵出了 牵出了一 出了一些 了一些接 一些接口 些接口权 接口权限 口权限和 权限和审 限和审计 和审计日 审计日志 计日志上 日志上的 志上的薄 上的薄弱 的薄弱点 这里 里把 把排 查过 过程 程整 整理 理成 成一 一篇 篇长 长期 期可 可复 复用 用的 的代 代码 码审 计思 思路 这里把 里把排 把排查 排查过 查过程 过程整 程整理 整理成 理成一 成一篇 一篇长 篇长期 长期可 期可复 可复用 复用的 用的代 的代码 代码审 码审计 审计思 计思路 这里把排 里把排查 把排查过 排查过程 查过程整 过程整理 程整理成 整理成一 理成一篇 成一篇长 一篇长期 篇长期可 长期可复 期可复用 可复用的 复用的代 用的代码 的代码审 代码审计 码审计思 审计思路 不涉 涉及 及攻 攻击 击利 利用 不涉及 涉及攻 及攻击 攻击利 击利用 不涉及攻 涉及攻击 及攻击利 攻击利用 只讨 讨论 论如 如何 何发 只讨论 讨论如 论如何 如何发
  29. 背景SSRF(Server-Side Request Forgery)是代码审计和漏洞复现里很常见的一类问题。它的风险不只在于“服务器帮用户发起请求”,更在于请求发起点往往位于内网、云环境或高权限网络区域,可能触达普通用户无法直接访问的资源。实际项目里,SSRF 经常出现在这些功能中:远程图片抓取、URL 预览、Webhook 回调、文件导入、PDF/截图生成、第三方资源转存、头像上传、站点连通性检测等。很多团队在修复时只做了简单黑名单,例如禁止 127.0.0.1,但真实场景里远远不够。本文只讨论在授权测试、代码审计和本地靶场中的复现与防护思路,不提供针对真实目标的攻击操作。典型场景假设一个业务提供“通过 URL 抓取图片并保存”的接口,后端接收用户传入的 URL,然后服务端下载资源。简化后的逻辑如下:POST /api/image/fetch Content-Type: application/json { \"url\": \" /> }常见的后端实现可能是这样的:// 伪代码 String url = request.getParameter(\"url\"); byte[] data = httpClient.get(url); storage.save(data);问题在于,如果没有严格限制 URL 的协议、解析结果、解析后的 IP 范围、跳转链路和响应大小,服务端就可能被诱导访问不该访问的地址。风险点拆解SSRF 的核心不在某一个 payload,而在“服务端请求边界失控”。审计时可以从以下几个维度判断风险。1. URL 协议是否受限只要允许用户控制完整 URL,就需要明确允许哪些协议。通常业务只需要 http 和 https。某些语言或库还可能支持 file、gopher、ftp、dict 等协议,默认行为不一定符合安全预期。推荐做法是使用白名单协议,而不是黑名单过滤:allowed_schemes = {\"http\", \"https\"} parsed = parse_url(user_input) if parsed.scheme not in allowed_schemes: reject()2. 域名解析后的 IP 是否可信很多修复只检查 URL 字符串是否包含 127.0.0.1、localhost,这种方式容易失效。更可靠的方式是:先规范化 URL,再解析域名,最后检查解析到的所有 IP 是否落入内网、回环、链路本地、保留地址等范围。需要重点拦截的地址范围包括:127.0.0.0/8:本机回环地址10.0.0.0/8、172.16.0.0/12、192.168.0.0/16:常见私网地址169.254.0.0/16:链路本地地址,云环境元数据服务经常相关::1/128:IPv6 回环地址fc00::/7、fe80::/10:IPv6 私有或链路本地地址0.0.0.0/8、224.0.0.0/4、240.0.0.0/4:特殊或保留地址一个简化的 Python 校验示例:import ipaddress import socket from urllib.parse import urlparse BLOCKED_NETS = [ ipaddress.ip_network(\"127.0.0.0/8\"), ipaddress.ip_network(\"10.0.0.0/8\"), ipaddress.ip_network(\"172.16.0.0/12\"), ipaddress.ip_network(\"192.168.0.0/16\"), ipaddress.ip_network(\"169.254.0.0/16\"), ipaddress.ip_network(\"0.0.0.0/8\"), ipaddress.ip_network(\"224.0.0.0/4\"), ipaddress.ip_network(\"240.0.0.0/4\"), ipaddress.ip_network(\"::1/128\"), ipaddress.ip_network(\"fc00::/7\"), ipaddress.ip_network(\"fe80::/10\"), ] def is_blocked_ip(ip): addr = ipaddress.ip_address(ip) return any(addr in net for net in BLOCKED_NETS) def resolve_and_check(url): parsed = urlparse(url) if parsed.scheme not in (\"http\", \"https\"): raise ValueError(\"unsupported scheme\") host = parsed.hostname if not host: raise ValueError(\"missing host\") results = socket.getaddrinfo(host, None) ips = {item[4][0] for item in results} for ip in ips: if is_blocked_ip(ip): raise ValueError(\"blocked target ip\") return True注意:生产环境里不能只在请求前检查一次。还要考虑 DNS Rebinding,即第一次解析为公网地址,通过校验后,实际连接时变成内网地址。更稳妥的方案是解析后绑定 IP 发起请求,并校验连接目标,或使用成熟的网络出口代理做统一拦截。3. HTTP 跳转链路是否重新校验很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只校验初始 URL,而跳转目标没有重新校验,依然可能导致访问越界。建议关闭自动跳转,手动处理每一次 Location,并对新的 URL 重新执行协议、域名、IP 范围校验,同时限制跳转次数。# 伪代码 max_redirects = 3 current_url = user_url for i in range(max_redirects): validate_url(current_url) resp = http_get(current_url, allow_redirects=False) if resp.status_code in [301, 302, 303, 307, 308]: current_url = join_url(current_url, resp.headers[\"Location\"]) continue return resp reject(\"too many redirects\")4. 响应大小和超时是否有限制SSRF 不一定只带来内网访问风险,也可能造成资源消耗。比如下载超大文件、慢响应、无限流式响应,都可能拖垮业务线程池或占满磁盘。建议至少设置以下限制:连接超时和读取超时,例如 2 到 5 秒最大响应体大小,例如图片抓取限制为 5MB 或 10MB最大重定向次数,例如不超过 3 次限制 Content-Type,但不要只依赖响应头流式读取时边读边计数,超过阈值立即中断# 伪代码 max_size = 5 * 1024 * 1024 total = 0 for chunk in response.iter_content(8192): total += len(chunk) if total &gt; max_size: raise ValueError(\"response too large\") write(chunk)5. 云环境元数据服务需要重点关注在云主机或容器环境中,元数据服务通常位于链路本地地址段。不同云厂商实现不同,但共同点是:应用如果能通过 SSRF 访问元数据接口,可能读取到实例相关信息或临时凭证。因此,在云环境里不能只依赖应用层校验,建议同时做网络层限制:应用所在安全组或防火墙限制访问元数据地址容器环境中限制 Pod 到元数据服务的访问开启云厂商提供的更安全元数据访问模式,例如要求 token 或限制 hop limit业务服务尽量使用最小权限的实例角色代码审计时的排查方法审计 SSRF 时,我通常先找“可控 URL 进入网络请求”的链路。关键不是搜索某一个函数,而是跟踪参数来源和最终 sink。可以关注这些关键词和调用点:语言/场景常见调用点关注点JavaURL.openConnection、HttpClient、OkHttp、RestTemplate是否校验协议、跳转和目标 IPPHPcurl_exec、file_get_contents、fopen是否允许远程 URL、是否限制协议Pythonrequests、urllib、httpx是否默认跟随跳转、是否设置超时Node.jsaxios、node-fetch、request是否限制重定向和私网地址Gohttp.Get、http.Client是否自定义 Transport 做地址校验常见业务参数名也值得留意:url、link、target、callback、redirectimage、avatar、file、import_urlwebhook、notify_url、callback_urlpreview、fetch、download、proxy如果是白盒审计,可以从路由入口开始做数据流分析:用户输入是否经过统一校验、是否被拼接成 URL、是否进入 HTTP 客户端、是否可能通过重定向改变请求目标。安全复现建议在授权测试中,建议搭建本地受控环境验证问题,不要直接对真实内网资源做探测。一个简单方法是准备两个本地服务:一个模拟业务服务,一个模拟内部资源。# 模拟内部资源服务,仅监听本机 python3 -m http.server 9000 --bind 127.0.0.1业务服务如果存在 SSRF 风险,传入一个指向本地资源的 URL,就能在本地日志中看到请求是否由服务端发起。这个过程能验证漏洞存在性,又不会影响第三方系统。也可以使用 DNS 日志平台或自建日志服务器确认“服务端是否发起了请求”,但不要继续扩大到端口扫描、凭证读取等高风险动作。漏洞报告里通常只需要证明服务端可控请求成立,并说明潜在影响即可。修复方案建议实际修复时,不建议只在业务代码里零散添加字符串过滤。更推荐做成统一组件或统一出口代理,所有远程资源抓取都走同一套策略。一个相对稳妥的修复清单:只允许 http、https 协议对 URL 做标准解析,不使用手写正则解析完整 URL解析域名后校验所有 A、AAAA 记录禁止访问私网、回环、链路本地、保留地址每次重定向后重新校验目标限制超时、响应大小、重定向次数限制可访问端口,通常只允许 80 和 443,必要时按业务白名单扩展关键业务使用域名白名单,而不是允许任意外部 URL云环境增加网络层和 IAM 最小权限控制记录安全日志,包括原始 URL、解析 IP、最终 URL、请求结果容易踩坑的点只过滤 localhost,但没有处理 127.0.0.1、IPv6、域名解析结果只校验第一次请求,没有校验重定向后的 Location认为 Content-Type 是 image/png 就一定是图片,实际响应头可伪造没有设置超时,导致线程被慢响应拖住没有限制下载大小,导致磁盘或内存被消耗在微服务环境中忽略服务发现、内部管理端口和本机 agent修复逻辑分散在多个接口,后续新功能又绕开校验总结SSRF 的防护重点不是记住某些特殊地址写法,而是建立一条清晰的服务端出网边界:哪些协议能用、哪些地址能访问、跳转是否可信、响应能消耗多少资源、云环境是否有额外隔离。从代码审计角度看,发现 SSRF 的关键是找到“用户可控输入到服务端网络请求”的完整链路;从修复角度看,最好把校验逻辑沉到统一组件或出口层,避免每个业务各写一套不完整的过滤。这样后续新增 URL 抓取、Webhook、资源转存等功能时,也能复用同一套安全边界。 代码审计、调用链与关键函数定位示意 背景 是代 代码 码审 审计 计和 和漏 漏洞 洞复 复现 现里 里很 很常 常见 见的 的一 一类 类问 问题 是代码 代码审 码审计 审计和 计和漏 和漏洞 漏洞复 洞复现 复现里 现里很 里很常 很常见 常见的 见的一 的一类 一类问 类问题 是代码审 代码审计 码审计和 审计和漏 计和漏洞 和漏洞复 漏洞复现 洞复现里 复现里很 现里很常 里很常见 很常见的 常见的一 见的一类 的一类问 一类问题 它的 的风 风险 险不 不只 只在 在于 它的风 的风险 风险不 险不只 不只在 只在于 它的风险 的风险不 风险不只 险不只在 不只在于 服务 务器 器帮 帮用 用户 户发 发起 起请 请求 服务器 务器帮 器帮用 帮用户 用户发 户发起 发起请 起请求 服务器帮 务器帮用 器帮用户 帮用户发 用户发起 户发起请 发起请求 更在 于请 求发 起点 点往 往往 往位 位于 于内 内网 更在于 在于请 于请求 请求发 求发起 发起点 起点往 点往往 往往位 往位于 位于内 于内网 更在于请 在于请求 于请求发 请求发起 求发起点 发起点往 起点往往 点往往位 往往位于 往位于内 位于内网 云环 环境 境或 或高 高权 权限 限网 网络 络区 区域 云环境 环境或 境或高 或高权 高权限 权限网 限网络 网络区 络区域 云环境或 环境或高 境或高权 或高权限 高权限网 权限网络 限网络区 网络区域 可能 能触 触达 达普 普通 通用 户无 无法 法直 直接 接访 访问 问的 的资 资源 可能触 能触达 触达普 达普通 普通用 通用户 用户无 户无法 无法直 法直接 直接访 接访问 访问的 问的资 的资源 可能触达 能触达普 触达普通 达普通用 普通用户 通用户无 用户无法 户无法直 无法直接 法直接访 直接访问 接访问的 访问的资 问的资源 实际 际项 项目 目里 实际项 际项目 项目里 实际项目 际项目里 经常 常出 出现 现在 在这 这些 些功 功能 能中 经常出 常出现 出现在 现在这 在这些 这些功 些功能 功能中 经常出现 常出现在 出现在这 现在这些 在这些功 这些功能 些功能中 远程 程图 图片 片抓 抓取 远程图 程图片 图片抓 片抓取 远程图片 程图片抓 图片抓取 预览 回调 文件 件导 导入 文件导 件导入 文件导入 截图 图生 生成 截图生 图生成 截图生成 第三 三方 方资 源转 转存 第三方 三方资 方资源 资源转 源转存 第三方资 三方资源 方资源转 资源转存 头像 像上 上传 头像上 像上传 头像上传 站点 点连 连通 通性 性检 检测 测等 站点连 点连通 连通性 通性检 性检测 检测等 站点连通 点连通性 连通性检 通性检测 性检测等 很多 多团 团队 队在 在修 修复 复时 时只 只做 做了 了简 简单 单黑 黑名 名单 很多团 多团队 团队在 队在修 在修复 修复时 复时只 时只做 只做了 做了简 了简单 简单黑 单黑名 黑名单 很多团队 多团队在 团队在修 队在修复 在修复时 修复时只 复时只做 时只做了 只做了简 做了简单 了简单黑 简单黑名 单黑名单 例如 如禁 禁止 例如禁 如禁止 例如禁止 但真 真实 实场 场景 景里 里远 远远 远不 不够 但真实 真实场 实场景 场景里 景里远 里远远 远远不 远不够 但真实场 真实场景 实场景里 场景里远 景里远远 里远远不 远远不够 本文 文只 只讨 讨论 论在 在授 授权 权测 测试 本文只 文只讨 只讨论 讨论在 论在授 在授权 授权测 权测试 本文只讨 文只讨论 只讨论在 讨论在授 论在授权 在授权测 授权测试 和本 本地 地靶 靶场 场中 中的 的复 现与 与防 防护 护思 思路 计和本 和本地 本地靶 地靶场 靶场中 场中的 中的复 的复现 复现与 现与防 与防护 防护思 护思路 审计和本 计和本地 和本地靶 本地靶场 地靶场中 靶场中的 场中的复 中的复现 的复现与 复现与防 现与防护 与防护思 防护思路 不提 提供 供针 针对 对真 实目 目标 标的 的攻 攻击 击操 操作 不提供 提供针 供针对 针对真 对真实 真实目 实目标 目标的 标的攻 的攻击 攻击操 击操作 不提供针 提供针对 供针对真 针对真实 对真实目 真实目标 实目标的 目标的攻 标的攻击 的攻击操 攻击操作 典型 型场 景假 假设 设一 一个 个业 业务 务提 典型场 型场景 场景假 景假设 假设一 设一个 一个业 个业务 业务提 务提供 典型场景 型场景假 场景假设 景假设一 假设一个 设一个业 一个业务 个业务提 业务提供 通过 取图 片并 并保 保存 抓取图 取图片 图片并
  30. SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替用户发一个 HTTP 请求”这么简单。真正需要关注的是:用户可控数据是否进入了服务端请求链路,服务端请求具有什么网络权限,响应内容是否会回显,以及应用是否错误地把网络位置当成了可信身份。 在实际审计中,SSRF 往往隐藏在图片抓取、Webhook 调试、在线导入、PDF 生成、链接预览、RSS 订阅和云资源配置等功能中。单独看某个请求参数,风险可能并不明显;但一旦结合内网可达性、云平台元数据接口、管理面板或未授权服务,就可能形成严重的数据泄露和横向影响。 一、先明确需要审计的问题审计 SSRF 时,不要只搜索 request、get 或 curl 等关键词。更有效的方式是沿着“输入—解析—请求—响应—输出”五个环节建立数据流: 输入:URL 是否来自请求参数、JSON 字段、文件内容、数据库记录或消息队列?解析:应用是否对协议、主机名、端口和重定向进行过校验?请求:使用了什么 HTTP 客户端,是否自动跟随重定向,是否支持 file、gopher 等非 HTTP 协议?响应:响应体、状态码、响应头或错误信息是否返回给用户?输出:请求结果是否被写入日志、缓存、文件、消息通知或后续任务?一个常见的误区是只检查目标 URL 的初始字符串。例如,代码可能拒绝了 127.0.0.1,却没有考虑域名解析后指向内网地址、IPv6 回环地址、十进制 IPv4 表示法,或者通过重定向跳转到受限网络。 二、一个典型的漏洞场景下面的示例是一个仅用于本地实验的链接预览接口。它接收用户提交的 URL,服务端抓取页面标题并返回结果: from flask import Flask, request, jsonify import requests app = Flask(__name__) @app.post('/preview') def preview(): target = request.form.get('url', '') response = requests.get(target, timeout=5) return jsonify({ 'status': response.status_code, 'title_source': response.text[:2000] })问题不在于 requests.get 本身,而在于 target 完全由外部输入控制。服务端可能访问本机服务、容器网络、企业内网或云环境中的内部接口。如果响应内容被完整返回,风险会进一步扩大;即使响应不回显,仍可能通过请求时间、状态码、错误信息或日志观察到内部服务是否存在。 在代码审计中,还应继续向上追踪 target 的来源。例如 URL 可能先进入数据库,再由定时任务异步处理;也可能由管理员配置,普通用户无法直接调用,但低权限用户能够影响配置内容。异步任务并不会天然降低风险,只是改变了触发方式。 三、在本地环境完成可控复现复现时建议搭建两个本地服务:一个模拟业务服务,另一个模拟内部服务。所有请求都限制在本机或专用测试网络,不要把测试目标指向互联网上的真实地址。 from flask import Flask app = Flask(__name__) @app.get('/internal-check') def internal_check(): return 'local test service only' app.run(host='127.0.0.1', port=9001)启动内部测试服务后,再调用业务接口: curl -X POST \ -d 'url= local test service only,说明用户输入确实穿透到了服务端请求层。这个验证已经足以证明存在 SSRF 数据流,不需要继续访问任何真实内网系统。 为了验证重定向处理是否存在缺陷,可以在本地再启动一个测试服务,让它返回 302 并指向另一个本地端口。重点观察 HTTP 客户端是否自动跟随,以及应用是在每次跳转前重新执行地址校验,还是只检查初始 URL。 四、审计中最容易漏掉的边界问题1. DNS 解析与 IP 校验不一致如果应用先校验域名字符串,再由 HTTP 客户端解析域名,校验对象和实际连接对象可能不是同一个。域名解析结果可能包含公网地址和内网地址,也可能随着时间变化。更稳妥的做法是: 解析域名得到所有 A 和 AAAA 记录。逐个判断是否属于回环、链路本地、私有、保留或其他禁止网段。连接时固定经过校验的解析结果,避免校验和连接之间再次发生不受控解析。在网络层配置出口策略,不能只依赖应用层判断。需要特别注意 IPv6。只过滤 127.0.0.0/8 并不能覆盖 ::1、链路本地地址以及其他本地或保留范围。 2. 重定向绕过初始校验服务端可能先检查一个公网 URL,但该 URL 返回重定向后,客户端自动访问了内网地址。建议默认关闭自动重定向;如果业务确实需要,应限制跳转次数,并对每个 Location 重新执行协议、主机、端口和 IP 校验。 3. URL 解析器差异不同语言、库和代理对 URL 的解析细节可能不同。用户名信息、端口、编码字符、大小写、尾随点、IPv6 方括号和非标准 IP 表示法,都可能造成“校验结果”和“实际连接目标”不一致。不要通过字符串前缀、简单正则或黑名单处理复杂 URL。 4. 协议和端口范围业务通常只需要 HTTPS 或 HTTP,就应采用明确的协议白名单,并限制端口范围。不要把“只禁止 file”当成完整防护,也不要默认所有 HTTP 客户端行为都相同。若底层库支持其他协议,应在适配层显式关闭或隔离。 五、一个更安全的校验思路下面示例展示的是防御思路,不是可以直接覆盖所有业务的通用组件。生产环境应结合语言运行时、HTTP 客户端和网络架构进行测试: from urllib.parse import urlparse import ipaddress import socket ALLOWED_SCHEMES = {'http', 'https'} ALLOWED_PORTS = {80, 443} def is_public_ip(value): address = ipaddress.ip_address(value) return not ( address.is_private or address.is_loopback or address.is_link_local or address.is_reserved or address.is_multicast or address.is_unspecified ) def validate_target(raw_url): parsed = urlparse(raw_url) if parsed.scheme 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') port = parsed.port or (443 if parsed.scheme == 'https' else 80) if port not in ALLOWED_PORTS: raise ValueError('port is not allowed') addresses = socket.getaddrinfo( parsed.hostname, port, type=socket.SOCK_STREAM ) for item in addresses: ip_value = item[4][0] if not is_public_ip(ip_value): raise ValueError('resolved address is not allowed') return parsed这段代码仍然不能单独解决 DNS 重绑定问题,因为“解析”和“建立连接”之间可能存在时间窗口。更可靠的实现通常包括自定义解析器、将解析结果传递给连接层、禁止自动跳转,并在出口防火墙或代理层再次限制目标网络。 六、修复不能只停留在代码层SSRF 的根本风险是应用服务器拥有过大的出网能力。因此建议采用分层防护: 业务层:对 URL 使用协议、域名、端口和解析结果白名单,默认关闭重定向。客户端层:设置连接超时、读取超时、响应体大小上限和最大跳转次数,避免请求被用于资源消耗。网络层:应用容器默认禁止访问管理网段、云元数据地址、数据库网段和内部控制面;需要访问的服务使用明确的出口代理。身份层:不要把高权限凭据放在应用可以随意访问的位置;云环境应使用最小权限身份,并采用平台提供的元数据访问保护机制。日志层:记录请求任务、目标域名、解析地址、最终连接地址、跳转链和拒绝原因,但注意脱敏 URL 中可能出现的凭据。对于图片抓取、文档转换等高风险功能,最好将网络访问放入独立沙箱。沙箱不应与主业务共享网络命名空间、云角色、敏感文件和长期凭据。 七、如何判断修复是否有效修复验证不能只测试一个 127.0.0.1。建议在本地测试环境建立覆盖矩阵: 测试类别预期结果允许的 HTTPS 公网测试域名请求成功,并受超时和大小限制HTTP 或 HTTPS 以外的协议在请求发出前拒绝本地回环地址和 IPv6 回环地址拒绝私有地址、链路本地地址和保留地址拒绝解析到受限地址的测试域名拒绝跳转到受限地址的本地测试服务跳转前或跳转后拒绝超大响应、慢响应和过多跳转按策略终止,不影响工作线程此外,还应通过单元测试覆盖 URL 解析、域名解析失败、IPv4 和 IPv6、显式端口、默认端口以及异常响应。若系统使用代理,必须分别测试“直连”和“代理请求”两条路径,因为代理可能重新解析域名,导致应用层的校验假设失效。 八、事件复盘中值得关注的信号如果怀疑某个链接抓取功能被滥用,可以从访问日志中寻找异常模式:短时间内大量不同主机名、访问内网保留地址、频繁出现连接超时、非正常端口、跳转链异常增长,以及请求目标与正常业务明显不符。日志中的目标地址不应直接等同于最终连接地址,最好同时记录解析结果和网络出口。 复盘时还要检查是否存在二次影响,例如响应内容是否进入缓存、是否被写入构建产物、是否通过通知系统发送给其他人员,以及抓取任务是否使用了高权限云身份。很多 SSRF 事件的影响并不发生在首次请求,而是发生在响应被后续组件信任之后。 总结SSRF 审计的关键不是寻找某个“危险 URL”,而是识别应用是否把外部输入转化成了受服务端网络权限保护的请求。可靠的防护应同时覆盖数据流、URL 解析、DNS 解析、重定向、协议限制、网络出口和身份权限。 复现时使用本地服务即可证明漏洞链路;修复时则要验证各种地址表示、解析结果和跳转行为,并把最终控制落到网络隔离和最小权限上。只有代码校验与基础设施策略同时生效,才不会因为一个解析器差异或一次重定向,让原本看似完善的防护失效。 服务器、云资产与权限边界梳理示意 服务 务端 端请 请求 求伪 伪造 服务端 务端请 端请求 请求伪 求伪造 服务端请 务端请求 端请求伪 请求伪造 并不 不是 并不是 让服 务器 器代 代替 替用 用户 户发 发一 一个 让服务 服务器 务器代 器代替 代替用 替用户 用户发 户发一 发一个 让服务器 服务器代 务器代替 器代替用 代替用户 替用户发 用户发一 户发一个 这么 么简 简单 这么简 么简单 这么简单 真正 正需 需要 要关 关注 注的 的是 真正需 正需要 需要关 要关注 关注的 注的是 真正需要 正需要关 需要关注 要关注的 关注的是 户可 可控 控数 数据 据是 是否 否进 进入 入了 了服 求链 链路 用户可 户可控 可控数 控数据 数据是 据是否 是否进 否进入 进入了 入了服 了服务 请求链 求链路 用户可控 户可控数 可控数据 控数据是 数据是否 据是否进 是否进入 否进入了 进入了服 入了服务 了服务端 端请求链 请求链路 求具 具有 有什 什么 么网 网络 络权 权限 请求具 求具有 具有什 有什么 什么网 么网络 网络权 络权限 端请求具 请求具有 求具有什 具有什么 有什么网 什么网络 么网络权 网络权限 响应 应内 内容 容是 否会 会回 回显 响应内 应内容 内容是 容是否 是否会 否会回 会回显 响应内容 应内容是 内容是否 容是否会 是否会回 否会回显 以及 及应 应用 用是 否错 错误 误地 地把 把网 络位 位置 置当 当成 成了 了可 可信 信身 身份 以及应 及应用 应用是 用是否 是否错 否错误 错误地 误地把 地把网 把网络 网络位 络位置 位置当 置当成 当成了 成了可 了可信 可信身 信身份 以及应用 及应用是 应用是否 用是否错 是否错误 否错误地 错误地把 误地把网 地把网络 把网络位 网络位置 络位置当 位置当成 置当成了 当成了可 成了可信 了可信身 可信身份 在实 实际 际审 审计 计中 在实际 实际审 际审计 审计中 在实际审 实际审计 际审计中 往往 往隐 隐藏 藏在 在图 图片 片抓 抓取 往往隐 往隐藏 隐藏在 藏在图 在图片 图片抓 片抓取 往往隐藏 往隐藏在 隐藏在图 藏在图片 在图片抓 图片抓取 调试 在线 线导 导入 在线导 线导入 在线导入 生成 链接 接预 预览 链接预 接预览 链接预览 订阅 阅和 和云 云资 资源 源配 配置 置等 等功 功能 能中 订阅和 阅和云 和云资 云资源 资源配 源配置 配置等 置等功 等功能 功能中 订阅和云 阅和云资 和云资源 云资源配 资源配置 源配置等 配置等功 置等功能 等功能中 单独 独看 看某 某个 个请 求参 参数 单独看 独看某 看某个 某个请 个请求 请求参 求参数 单独看某 独看某个 看某个请 某个请求 个请求参 请求参数 风险 险可 可能 能并 不明 明显 风险可 险可能 可能并 能并不 并不明 不明显 风险可能 险可能并 可能并不 能并不明 并不明显 但一 一旦 旦结 结合 合内 内网 网可 可达 达性 但一旦 一旦结 旦结合 结合内 合内网 内网可 网可达 可达性 但一旦结 一旦结合 旦结合内 结合内网 合内网可 内网可达 网可达性 云平 平台 台元 元数 据接 接口 云平台 平台元 台元数 元数据 数据接 据接口 云平台元 平台元数 台元数据 元数据接 数据接口 管理 理面 面板 板或 或未 未授 授权 权服 管理面 理面板 面板或 板或未 或未授 未授权 授权服 权服务 管理面板 理面板或 面板或未 板或未授 或未授权 未授权服 授权服务 就可 能形 形成 成严 严重 重的 的数 据泄 泄露 露和 和横 横向 向影 影响 就可能 可能形 能形成 形成严 成严重 严重的 重的数 的数据 数据泄 据泄露 泄露和 露和横 和横向 横向影 向影响 就可能形 可能形成 能形成严 形成严重 成严重的 严重的数 重的数据 的数据泄 数据泄露 据泄露和 泄露和横 露和横向 和横向影 横向影响 先明 明确 确需 要审 计的 的问 问题 题审 先明确 明确需 确需要 需要审 要审计 审计的 计的问 的问题 问题审 题审计 先明确需 明确需要 确需要审 需要审计 要审计的 审计的问 计的问题 的问题审 问题审计 不要 要只 只搜 搜索 不要只 要只搜 只搜索 不要只搜 要只搜索 等关 关键 键词 等关键 关键词 等关键词 更有 有效 效的 的方 方式 式是 是沿 沿着 更有效 有效的 效的方 的方式 方式是 式是沿 是沿着 更有效的 有效的方 效的方式 的方式是 方式是沿 式是沿着 输入 解析 输出 五个 个环 环节 节建 建立 立数 据流 五个环