<?xml version="1.0"?>
<rss version="2.0"><channel><title>&#x5E94;&#x6025;&#x54CD;&#x5E94;&#x4E2D;&#x5FC3;&#x90E8;&#x843D;&#x683C;</title><link>https://www.hackhat.org/blogs/blog/1-%E5%BA%94%E6%80%A5%E5%93%8D%E5%BA%94%E4%B8%AD%E5%BF%83%E9%83%A8%E8%90%BD%E6%A0%BC/</link><description></description><language>zh</language><item><title>&#x4E00;&#x6B21; Java Web SSRF &#x98CE;&#x9669;&#x7684;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E0E;&#x4FEE;&#x590D;&#x601D;&#x8DEF;</title><link>https://www.hackhat.org/blogs/entry/296-%E4%B8%80%E6%AC%A1-java-web-ssrf-%E9%A3%8E%E9%99%A9%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%8E%E4%BF%AE%E5%A4%8D%E6%80%9D%E8%B7%AF/</link><description><![CDATA[<h2>背景</h2><p>SSRF（Server-Side Request Forgery）在企业内部系统里并不少见，尤其是带有“URL 预览、远程图片抓取、Webhook 回调、文件导入、第三方接口代理”等能力的业务。它的问题不在于某一行代码有多复杂，而是服务端代替用户发起了网络请求，如果边界没收紧，就可能访问到本不该暴露的内部资源。</p><p>这篇文章整理一次比较典型的 Java Web 代码审计过程，重点放在识别风险点、分析常见绕过、设计可落地的修复方案。内容偏防守视角，不涉及对真实目标的攻击操作。</p><h2>场景描述</h2><p>业务里有一个“远程资源导入”功能，用户提交一个 URL，后端拉取资源并保存到对象存储。简化后的代码如下：</p><pre><code>@PostMapping("/import")
public ResponseEntity&lt;String&gt; importResource(@RequestParam String url) throws Exception {
    RestTemplate restTemplate = new RestTemplate();
    byte[] data = restTemplate.getForObject(url, byte[].class);
    String objectKey = storageService.save(data);
    return ResponseEntity.ok(objectKey);
}</code></pre><p>从功能上看很直接，但从安全角度看，风险也很明显：用户可控的 URL 被服务端直接请求，没有协议限制、没有目标地址校验、没有重定向控制、没有响应大小限制，也没有请求超时策略。</p><h2>风险点分析</h2><h2>1. 协议未限制</h2><p>很多代码只判断字符串是否以 <code>http</code> 开头，甚至完全不判断协议。这样容易引入非预期协议访问风险。即使当前 HTTP 客户端默认只支持 HTTP/HTTPS，也不建议把安全假设建立在库的默认行为上。</p><p>建议明确只允许：</p><ul><li><code>http</code></li><li><code>https</code></li></ul><p>其他协议直接拒绝。</p><h2>2. 域名与 IP 校验缺失</h2><p>SSRF 的核心问题通常是访问内部地址，例如内网网段、本机地址、链路本地地址等。审计时需要重点检查是否存在以下情况：</p><ul><li>允许访问 <code>127.0.0.1</code>、<code>localhost</code></li><li>允许访问 <code>10.0.0.0/8</code>、<code>172.16.0.0/12</code>、<code>192.168.0.0/16</code></li><li>允许访问 <code>169.254.0.0/16</code> 这类链路本地地址</li><li>只校验域名字符串，不校验解析后的 IP</li><li>只校验第一次请求，不校验重定向后的目标</li></ul><p>仅靠黑名单字符串拦截通常不可靠，因为域名解析、IPv6、十进制/八进制 IP 表示、重定向等都会让判断变复杂。更稳妥的方式是做“允许列表”或解析后的地址校验。</p><h2>3. 重定向未重新校验</h2><p>不少 HTTP 客户端会自动跟随 30x 重定向。如果业务只校验初始 URL，没有校验跳转后的 URL，那么校验就可能失效。</p><p>更安全的做法是关闭自动重定向，手动处理每一次跳转，并在每次跳转前重新执行完整校验，同时限制跳转次数。</p><h2>4. 响应大小与超时缺失</h2><p>即使没有访问内部资源，攻击者也可能提交一个响应极慢或响应体极大的地址，导致线程阻塞、内存占用升高，最终影响服务稳定性。因此 SSRF 防护不只关心“访问哪里”，也要关心“请求多久、下载多少”。</p><p>建议至少设置：</p><ul><li>连接超时</li><li>读取超时</li><li>最大响应体大小</li><li>最大重定向次数</li><li>允许的 Content-Type 范围</li></ul><h2>审计思路</h2><p>做 Java Web 代码审计时，可以从“用户输入到网络请求”的数据流入手。常见危险调用包括：</p><ul><li><code>RestTemplate#getForObject</code></li><li><code>RestTemplate#exchange</code></li><li><code>HttpURLConnection#connect</code></li><li><code>OkHttpClient#newCall</code></li><li><code>Apache HttpClient#execute</code></li><li><code>URL#openStream</code></li><li><code>ImageIO#read(URL)</code></li></ul><p>可以先用代码搜索定位关键调用：</p><pre><code>grep -R "RestTemplate\|openStream\|HttpURLConnection\|OkHttpClient\|ImageIO.read" -n src/main/java</code></pre><p>定位后再向上追踪参数来源，判断 URL 是否来自用户输入、配置中心、消息队列、数据库字段或第三方回调。很多风险并不直接出现在 Controller 层，而是在异步任务、导入任务、图片处理服务中。</p><h2>关键修复方案</h2><h2>1. 使用 URI 解析而不是字符串拼接判断</h2><p>不要用简单的 <code>startsWith</code> 或正则处理完整 URL。应使用标准 URI 解析，提取 scheme、host、port 等字段后再判断。</p><pre><code>URI uri = URI.create(inputUrl);
String scheme = uri.getScheme();
String host = uri.getHost();

if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
    throw new IllegalArgumentException("unsupported scheme");
}

if (host == null || host.isBlank()) {
    throw new IllegalArgumentException("invalid host");
}</code></pre><h2>2. 解析域名并校验所有 IP</h2><p>如果允许用户提交域名，应解析该域名对应的所有 IP，并逐一判断是否落在禁止访问的地址范围内。</p><pre><code>private boolean isPrivateOrLocalAddress(InetAddress address) {
    return address.isAnyLocalAddress()
            || address.isLoopbackAddress()
            || address.isLinkLocalAddress()
            || address.isSiteLocalAddress()
            || address.isMulticastAddress();
}

private void validateHost(String host) throws UnknownHostException {
    InetAddress[] addresses = InetAddress.getAllByName(host);
    for (InetAddress address : addresses) {
        if (isPrivateOrLocalAddress(address)) {
            throw new IllegalArgumentException("target address is not allowed");
        }
    }
}</code></pre><p>这里的示例主要覆盖常见本地、内网、链路本地地址。实际生产环境还应根据网络架构补充公司内部网段、云厂商元数据地址、办公网段等。</p><h2>3. 优先采用业务允许列表</h2><p>如果业务场景明确，例如只允许导入几个合作方的资源，最好直接使用域名允许列表。相比维护复杂黑名单，允许列表更容易解释和验证。</p><pre><code>private static final Set&lt;String&gt; ALLOWED_HOSTS = Set.of(
    "static.example.com",
    "assets.partner.example"
);

private void validateAllowedHost(String host) {
    String normalized = host.toLowerCase(Locale.ROOT);
    if (!ALLOWED_HOSTS.contains(normalized)) {
        throw new IllegalArgumentException("host is not allowed");
    }
}</code></pre><p>如果需要支持子域名，也建议明确后缀匹配规则，避免 <code>example.com.evil.test</code> 这类误判。可以采用“等于主域名或以点加主域名结尾”的方式：</p><pre><code>private boolean matchDomain(String host, String domain) {
    host = host.toLowerCase(Locale.ROOT);
    domain = domain.toLowerCase(Locale.ROOT);
    return host.equals(domain) || host.endsWith("." + domain);
}</code></pre><h2>4. 禁止自动重定向，逐跳校验</h2><p>使用 Apache HttpClient 时，可以关闭自动重定向，再根据响应头中的 <code>Location</code> 手动处理。每一次跳转都要重新解析 URL、校验协议、校验 Host、解析 IP，并限制跳转次数。</p><pre><code>RequestConfig config = RequestConfig.custom()
        .setConnectTimeout(3000)
        .setResponseTimeout(5000)
        .setRedirectsEnabled(false)
        .build();

CloseableHttpClient client = HttpClients.custom()
        .setDefaultRequestConfig(config)
        .disableRedirectHandling()
        .build();</code></pre><p>重定向处理建议控制在 3 次以内。超过次数直接失败，不要为了兼容性无限跳转。</p><h2>5. 限制响应体大小</h2><p>不要直接把响应一次性读入内存。可以边读边统计大小，超过阈值立即中断。</p><pre><code>private byte[] readWithLimit(InputStream input, int maxBytes) throws IOException {
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    byte[] buffer = new byte[8192];
    int total = 0;
    int n;

    while ((n = input.read(buffer)) != -1) {
        total += n;
        if (total &gt; maxBytes) {
            throw new IOException("response too large");
        }
        output.write(buffer, 0, n);
    }
    return output.toByteArray();
}</code></pre><p>具体阈值要结合业务，例如头像类资源可以限制到几 MB，普通文档导入可以再放宽，但不建议无限制。</p><h2>一套相对完整的校验流程</h2><p>可以把服务端请求外部 URL 的流程整理成固定模板，避免不同业务各写一套半成品校验：</p><ul><li>解析 URI，拒绝非法 URL</li><li>只允许 HTTP/HTTPS</li><li>校验 Host 是否在允许范围内</li><li>解析 Host 对应的全部 IP</li><li>拒绝本地地址、内网地址、链路本地地址和公司内部敏感网段</li><li>设置连接超时和读取超时</li><li>关闭自动重定向，逐跳重新校验</li><li>限制最大响应大小</li><li>限制允许的响应类型</li><li>记录审计日志，便于排查异常请求</li></ul><h2>测试用例建议</h2><p>修复后建议补充单元测试和集成测试。测试重点不是“能不能请求成功”，而是边界是否被正确拒绝。</p><table><tr><td>测试项</td><td>预期结果</td></tr><tr><td>HTTP/HTTPS 正常公网域名</td><td>允许，前提是符合业务允许范围</td></tr><tr><td>file、ftp 等非 HTTP 协议</td><td>拒绝</td></tr><tr><td>localhost、本机回环地址</td><td>拒绝</td></tr><tr><td>内网 IPv4 地址</td><td>拒绝</td></tr><tr><td>IPv6 本地地址</td><td>拒绝</td></tr><tr><td>公网地址重定向到内网地址</td><td>拒绝</td></tr><tr><td>响应体超过限制</td><td>中断并返回失败</td></tr><tr><td>连接超时或读取超时</td><td>失败且不阻塞主流程</td></tr></table><p>如果团队内有统一安全测试环境，可以搭一个只用于验证的本地 HTTP 服务，模拟重定向、超大响应、慢响应等情况。注意测试目标应限定在本地或授权环境内。</p><h2>日志与监控</h2><p>SSRF 类问题修复后，也建议保留必要的审计日志。日志不要记录完整敏感参数，但应能支持排查：</p><ul><li>调用方用户或业务标识</li><li>原始 Host</li><li>解析后的 IP 列表</li><li>最终请求结果</li><li>失败原因</li><li>耗时与响应大小</li></ul><p>如果短时间内出现大量被拒绝的内网地址、异常协议、重定向失败，可以作为安全告警信号。</p><h2>常见误区</h2><ul><li>只用正则判断 URL 格式，未解析真实 Host。</li><li>只拦截 <code>127.0.0.1</code>，忽略 IPv6、本机名、内网网段。</li><li>只校验第一次请求，忽略重定向。</li><li>认为业务在内网就安全，实际上服务端能访问的资源往往更多。</li><li>直接复用通用 HTTP 客户端，没有设置超时和大小限制。</li><li>把 SSRF 防护写在某个 Controller，其他异步任务继续裸请求。</li></ul><h2>总结</h2><p>SSRF 的修复重点不是单个判断条件，而是建立一条完整的服务端出站请求安全链路。对代码审计来说，重点关注用户可控 URL 是否进入网络请求 API；对修复来说，优先考虑允许列表、解析后 IP 校验、重定向逐跳校验、超时和响应大小限制。</p><p>实际项目中，建议把这套逻辑沉淀成统一组件，禁止业务代码直接使用裸 HTTP 客户端访问用户输入的 URL。这样既能减少重复实现，也能避免后续新增功能时再次引入同类问题。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">296</guid><pubDate>Sun, 11 Oct 2026 23:50:04 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21; WebShell &#x544A;&#x8B66;&#x5230;&#x6839;&#x56E0;&#x5B9A;&#x4F4D;&#xFF1A;Web &#x5165;&#x4FB5;&#x4E8B;&#x4EF6;&#x7684;&#x53D6;&#x8BC1;&#x4E0E;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x65B9;&#x6CD5;</title><link>https://www.hackhat.org/blogs/entry/295-%E4%BB%8E%E4%B8%80%E6%AC%A1-webshell-%E5%91%8A%E8%AD%A6%E5%88%B0%E6%A0%B9%E5%9B%A0%E5%AE%9A%E4%BD%8D%EF%BC%9Aweb-%E5%85%A5%E4%BE%B5%E4%BA%8B%E4%BB%B6%E7%9A%84%E5%8F%96%E8%AF%81%E4%B8%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E6%96%B9%E6%B3%95/</link><description><![CDATA[<p>在实际的 Web 安全事件中，最难处理的往往不是发现某个可疑文件，而是回答三个问题：攻击者从哪里进入、哪些数据可能被读取或篡改、修复后是否还存在残留访问路径。只删除一个所谓的“木马文件”，通常无法完成处置，甚至可能破坏后续取证。</p><p>本文以一次典型的 WebShell 告警为背景，整理从告警确认、证据保全、日志关联到代码审计和修复验证的完整思路。示例以常见的 PHP 应用为主，但其中的方法同样适用于 Java、Node.js 和 Python Web 服务。</p><h2>一、事件背景与初步判断</h2><p>假设主机安全产品报告 Web 目录下出现可疑 PHP 文件，文件内容包含动态执行函数，且该文件在凌晨被访问过。此时不应立即删除文件或重启主机，而应先确认告警是否具备真实攻击特征。</p><p>初步判断可以围绕以下几个维度展开：</p><ul><li>文件的创建时间、修改时间和属主是否异常，是否与发布窗口一致。</li><li>访问日志中是否存在对该文件的外部请求，以及请求来源、User-Agent 和响应状态。</li><li>文件是否被其他脚本包含，或者是否存在同目录下的关联文件。</li><li>应用进程、计划任务、启动项和 Web 服务配置是否出现异常变更。</li><li>数据库、对象存储和业务日志中是否出现异常读取或批量修改。</li></ul><p>其中，单独依赖文件名或关键字匹配容易产生误判。某些业务代码确实会使用反射、模板执行或动态加载机制；相反，攻击者也可能使用普通文件名、拆分字符串或编码内容来规避简单扫描。</p><h2>二、先保全证据，再进行清理</h2><p>事件响应的第一原则是保留现场。对于疑似入侵主机，建议先限制其对外访问范围，保留管理通道，并在不改变原始证据的前提下进行采集。若业务无法停机，可以先将流量切换到干净实例，再对原实例做离线分析。</p><p>采集时至少应保存以下内容：</p><ul><li>Web 目录、上传目录、临时目录和应用发布目录的文件清单。</li><li>Web 访问日志、错误日志、反向代理日志和 WAF 告警记录。</li><li>应用部署记录、版本控制提交记录以及 CI/CD 执行记录。</li><li>系统登录日志、进程列表、监听端口、计划任务和服务配置。</li><li>数据库审计日志、对象存储访问日志以及密钥使用记录。</li></ul><p>在 Linux 环境下，可以先生成文件清单和哈希，后续再对比文件变化。下面的命令只用于证据采集，不会修改目标文件：</p><pre><code>mkdir -p /secure/evidence
find /var/www/app -type f -printf '%T@ %s %p\n' | sort -n &gt; /secure/evidence/files.txt
find /var/www/app -type f -print0 | xargs -0 sha256sum &gt; /secure/evidence/sha256.txt
ps auxww &gt; /secure/evidence/processes.txt
ss -lntup &gt; /secure/evidence/listeners.txt
crontab -l &gt; /secure/evidence/cron-current.txt 2&gt;/dev/null || true</code></pre><p>实际操作中应记录采集时间、主机标识、操作者和命令输出的校验值。涉及内存、容器层或云主机快照时，还需要结合平台能力保留快照和审计事件，避免只分析文件系统而遗漏驻留进程。</p><h2>三、通过日志还原攻击链</h2><p>单条访问日志很难说明问题，应把 WAF、反向代理、Web 服务器和应用日志放到同一时间线中。重点关注以下模式：</p><ul><li>短时间内对多个不存在路径进行探测，随后访问上传、管理或调试接口。</li><li>同一来源先提交异常参数，之后访问新出现的脚本文件。</li><li>请求方法、Content-Type、响应状态和响应体大小出现明显变化。</li><li>访问时间与文件创建时间高度接近，且请求来源并非正常办公网络或发布系统。</li><li>出现大量 500 错误、异常堆栈、路径泄露或模板渲染错误。</li></ul><p>建议将日志统一转换为时间戳，并以请求 ID、客户端地址、反向代理追踪头和用户会话标识进行关联。需要特别注意代理链中的真实来源地址：不能直接相信任意客户端提交的 X-Forwarded-For，只有在明确配置的可信代理范围内才应使用该字段。</p><p>如果访问日志中包含敏感参数，不应直接在工单或聊天窗口复制传播。分析时可以对令牌、Cookie、身份证号等字段做脱敏，并限制证据文件的访问权限。</p><h2>四、代码审计：从文件落地位置反推入口</h2><p>在很多事件中，可疑文件只是最终结果，真正的漏洞位于文件上传、解压、编辑器接口、插件安装或调试功能。审计应从文件写入点开始，而不是只围绕可疑文件做字符串搜索。</p><p>重点搜索以下类型的代码路径：</p><ul><li>将用户输入拼接到文件路径、模板路径或命令参数中的位置。</li><li>使用原始文件名、扩展名或 MIME 类型决定保存方式的逻辑。</li><li>从请求参数读取内容后直接写入磁盘的接口。</li><li>对压缩包进行解压但没有校验目录穿越的功能。</li><li>把上传目录放在可被 Web 服务器解释执行的目录下。</li><li>仅依赖前端校验、黑名单扩展名或客户端提交的 Content-Type。</li></ul><p>下面是一个常见但不安全的上传逻辑示例。问题不在于是否使用了白名单，而在于最终文件仍然保存于可执行目录，且文件名和内容校验都不充分：</p><pre><code>$name = $_FILES['file']['name'];
$target = __DIR__ . '/uploads/' . $name;
move_uploaded_file($_FILES['file']['tmp_name'], $target);</code></pre><p>更稳妥的设计应同时满足“存储位置不可执行、服务端重新生成文件名、校验真实文件类型、限制大小和权限、记录审计信息”等条件。示例代码如下，具体 MIME 检测和图片解码策略仍应结合业务类型完善：</p><pre><code>$tmp = $_FILES['file']['tmp_name'];
$size = (int) $_FILES['file']['size'];

if ($size &lt;= 0 || $size &gt; 5 * 1024 * 1024) {
    throw new RuntimeException('invalid size');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo-&gt;file($tmp);
$allowed = array('image/jpeg', 'image/png', 'application/pdf');
if (!in_array($mime, $allowed, true)) {
    throw new RuntimeException('invalid type');
}

$storage = '/srv/app-data/uploads';
$id = bin2hex(random_bytes(16));
$target = $storage . '/' . $id;
if (!move_uploaded_file($tmp, $target)) {
    throw new RuntimeException('save failed');
}
chmod($target, 0640);</code></pre><p>对于图片文件，不能只检查 MIME。应尝试使用图像库重新解码并重新编码，去除不需要的元数据；对于 PDF 等复杂格式，应在隔离环境中处理，并通过内容安全策略限制在线预览。上传目录最好位于 Web 根目录之外，若业务必须直接访问，应通过下载接口读取，并设置正确的 Content-Type、Content-Disposition 和权限控制。</p><h2>五、如何判断是否存在持久化或横向影响</h2><p>确认 Web 目录中的异常文件后，还要排查攻击者是否建立了其他访问路径。检查范围不应只限于当前应用目录，还应覆盖：</p><ul><li>系统级和用户级计划任务、systemd 服务、容器启动参数。</li><li>Web 服务器配置、反向代理规则、应用启动脚本和环境变量。</li><li>新增用户、SSH 授权密钥、异常 sudo 配置和临时凭据。</li><li>应用依赖包、插件、构建产物和镜像基础层是否被污染。</li><li>数据库中的管理员账户、API 密钥、回调地址和定时任务。</li><li>同一凭据访问过的其他主机、对象存储和代码仓库。</li></ul><p>这里要区分“观察到的事实”和“推测的影响”。例如，发现某个脚本读取了数据库连接配置，只能说明存在读取可能；是否真的读取了业务数据，还需要结合数据库审计、应用日志、网络流量或访问结果进行确认。事件报告中应明确标注证据等级，避免把推测写成结论。</p><h2>六、清理、修复与验证</h2><p>清理不应止于删除单个文件。更可靠的方式是从可信代码仓库重新构建应用，并在全新实例上部署。对于无法立即重建的环境，至少应执行以下步骤：</p><ul><li>隔离受影响实例，撤销或轮换数据库密码、云访问密钥、应用密钥和管理员会话。</li><li>移除确认的恶意文件及其关联持久化项，并保存删除前的证据副本。</li><li>修复导致文件落地或未授权访问的代码路径，补充服务端校验和权限控制。</li><li>检查 Web 进程的最小权限，禁止其写入代码目录和配置目录。</li><li>将上传、缓存、日志和临时文件分离，并为敏感目录设置不可执行策略。</li><li>在测试环境回归验证，再通过灰度发布上线。</li></ul><p>修复后的验证应包括正向和反向测试：合法文件能够正常上传和访问；伪造扩展名、错误 MIME、超大文件、路径特殊字符、压缩包目录穿越和重复提交等情况都应被拒绝。还要验证异常请求不会把详细堆栈、绝对路径或敏感配置返回给客户端。</p><h2>七、容易忽略的几个问题</h2><ul><li><strong>只删文件不查入口：</strong>如果上传或写文件漏洞仍然存在，清理很快会失效。</li><li><strong>只看 Web 日志：</strong>攻击者可能通过应用任务、消息队列、对象存储或管理接口触发行为。</li><li><strong>过度依赖杀毒软件：</strong>特征库只能帮助发现已知样本，不能替代权限、日志和代码审计。</li><li><strong>忽略凭据轮换：</strong>即使没有证据证明密钥被读取，只要主机曾被完全控制，就应按泄露处理。</li><li><strong>没有建立基线：</strong>缺少发布记录和文件哈希时，很难区分正常变更与恶意变更。</li><li><strong>直接在线分析样本：</strong>处理可疑文件时应使用隔离环境，关闭不必要的网络访问，避免触发外联或二次污染。</li></ul><h2>八、建议建立的长期防护机制</h2><p>一次事件复盘的价值，不只是修补一个漏洞，而是把经验固化到开发、部署和监控流程中。比较实用的改进包括：</p><ul><li>在 CI 阶段执行依赖漏洞扫描、静态代码分析和敏感配置检测。</li><li>对高风险文件写入、权限变更和管理接口建立审计日志。</li><li>为 Web 目录、上传目录和发布目录建立文件完整性基线。</li><li>使用不可变部署或只读容器文件系统，减少运行时写入面。</li><li>对后台接口启用多因素认证、来源限制和细粒度权限控制。</li><li>将 WAF 告警与主机文件变更、应用发布记录和身份日志关联起来。</li><li>定期开展备份恢复演练，确认备份未被同一凭据和权限链路污染。</li></ul><h2>总结</h2><p>WebShell 或异常脚本告警只是一个入口，真正的分析目标是还原完整的攻击路径和影响范围。稳妥的流程应当是：先保全证据，再关联日志；从异常文件反推写入入口；检查持久化、凭据和横向影响；最后通过可信重建、凭据轮换和回归测试完成修复。</p><p>在技术细节之外，事件响应还需要保持证据意识和边界意识。能被日志证明的事实、基于代码推导出的可能性、以及尚未确认的风险，应分别记录。这样既能提高处置质量，也能让后续开发和运维团队明确知道应该修复什么、验证什么，以及如何避免同类问题再次发生。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">295</guid><pubDate>Sun, 11 Oct 2026 23:42:05 +0000</pubDate></item><item><title>&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x5230;&#x4E8B;&#x4EF6;&#x590D;&#x76D8;&#xFF1A;&#x7CFB;&#x7EDF;&#x5316;&#x5206;&#x6790;&#x4E0E;&#x6CBB;&#x7406; SSRF &#x98CE;&#x9669;</title><link>https://www.hackhat.org/blogs/entry/294-%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%88%B0%E4%BA%8B%E4%BB%B6%E5%A4%8D%E7%9B%98%EF%BC%9A%E7%B3%BB%E7%BB%9F%E5%8C%96%E5%88%86%E6%9E%90%E4%B8%8E%E6%B2%BB%E7%90%86-ssrf-%E9%A3%8E%E9%99%A9/</link><description><![CDATA[<p>SSRF（服务端请求伪造）并不是“让服务器替客户端发一个 HTTP 请求”这么简单。真正难处理的地方在于：URL 解析存在差异、DNS 解析结果可能变化、重定向会改变目标，以及应用所在网络通常比普通用户拥有更高的访问权限。很多实际事件并非由单一漏洞触发，而是由“任意 URL 请求能力 + 缺少出口控制 + 内部服务认证薄弱”共同造成。</p>

<h2>一、问题场景与风险边界</h2>
<p>常见业务功能包括图片抓取、远程文件导入、Webhook 测试、在线网页预览、PDF 转换和第三方接口代理。这些功能本身并不等于漏洞，关键在于服务端是否允许用户完全控制请求目标，以及请求是否能够访问不应暴露的网络区域。</p>
<ul>
<li>应用服务器访问本机回环地址、内网地址或链路本地地址。</li>
<li>应用所在网络可以访问管理面板、配置中心、监控接口等内部服务。</li>
<li>请求组件默认跟随重定向，导致初始校验通过后又跳转到受限地址。</li>
<li>域名校验和实际连接之间存在时间差，DNS 结果发生变化后仍继续连接。</li>
<li>日志只记录用户输入的 URL，没有记录最终解析地址、重定向链和出口策略。</li>
</ul>
<p>在授权测试或内部实验环境中，应使用专门的模拟服务验证行为，不要直接探测生产内网、云平台元数据或第三方系统。</p>

<h2>二、代码审计时的定位方法</h2>
<p>审计时不要只搜索“SSRF”或某个 HTTP 库的函数名，而应沿着数据流寻找“用户可控字符串如何变成网络连接目标”。重点关注以下输入来源：</p>
<ul>
<li>HTTP 参数、JSON 字段、表单字段和导入文件中的 URL。</li>
<li>Webhook、回调地址、图片地址、文件下载地址和代理配置。</li>
<li>经过数据库、消息队列或缓存转发后再次进入请求模块的值。</li>
</ul>
<p>常见的危险汇点包括 Python 的 requests、urllib、Java 的 URLConnection、HttpClient，Node.js 的 fetch、axios，以及 Go 的 http.Client。审计时还要继续跟踪封装层，很多项目会把实际请求隐藏在“下载器”“预览器”或“远程资源解析器”中。</p>
<p>一个有价值的审计记录至少应包含：输入来源、URL 解析位置、协议限制、主机校验、端口限制、DNS 解析方式、是否允许重定向、连接超时、响应大小限制和出口网络策略。只标记“这里调用了 HTTP 客户端”通常不足以判断实际风险。</p>

<h2>三、URL 校验中最容易出现的误区</h2>
<ul>
<li>只判断字符串是否以某个域名开头。解析 URL 时应以标准解析器得到的 hostname 为准，而不是直接进行字符串比较。</li>
<li>只阻断 127.0.0.1。还需要考虑 IPv6 回环地址、IPv4 映射地址、私有地址、链路本地地址、未指定地址和其他保留网段。</li>
<li>只校验域名一次。域名可能解析出多个 A 或 AAAA 记录，不能只检查第一个结果。</li>
<li>允许自动跟随重定向。每一次 Location 跳转都必须重新执行协议、主机、端口和 IP 策略校验。</li>
<li>将 DNS 解析和建立连接分成两个不受控制的步骤。校验通过的解析结果不应在实际连接时被重新解析成另一组地址。</li>
<li>把“域名在白名单中”当成完整安全边界。白名单域名被接管、配置错误或返回重定向时，仍可能成为风险入口。</li>
</ul>

<h2>四、一个用于审计的安全校验示例</h2>
<p>下面的代码只演示解析和地址判定，不负责实际发起请求。生产实现还需要结合固定出口代理、连接地址绑定和完整的错误处理。示例的重点是：先规范化解析，再检查所有解析结果，而不是用字符串替换或正则表达式代替 URL 解析。</p>
<pre><code>import ipaddress
import socket
from urllib.parse import urlsplit

ALLOWED_SCHEMES = {'http', 'https'}
ALLOWED_PORTS = {80, 443}

def validate_target(raw_url):
    parsed = urlsplit(raw_url)
    scheme = parsed.scheme.lower()

    if scheme not in ALLOWED_SCHEMES:
        raise ValueError('scheme denied')
    if parsed.username or parsed.password:
        raise ValueError('userinfo denied')
    if not parsed.hostname:
        raise ValueError('host missing')

    try:
        port = parsed.port or (443 if scheme == 'https' else 80)
    except ValueError:
        raise ValueError('invalid port')

    if port not in ALLOWED_PORTS:
        raise ValueError('port denied')

    host = parsed.hostname.rstrip('.').lower()
    addresses = set()
    for item in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM):
        addresses.add(item[4][0])

    if not addresses:
        raise ValueError('no address')

    for address in addresses:
        ip = ipaddress.ip_address(address)
        if not ip.is_global:
            raise ValueError('non-public address denied')

    return scheme, host, port, addresses</code></pre>
<p>这个示例仍有几个工程化要求：生产环境应明确处理域名大小写和尾部点号，统一 IPv4 与 IPv6 的表示；应记录实际解析到的地址；建立连接时要确保使用经过校验的地址，避免再次触发不受控解析；同时还要设置连接超时、读取超时、响应大小上限和总耗时上限。</p>

<h2>五、推荐的防护架构</h2>
<p>单靠业务代码中的黑名单往往难以长期维护，更稳妥的做法是将应用层校验与网络出口控制结合起来。</p>
<ul>
<li><strong>协议和端口最小化：</strong>业务不需要的协议全部禁用，端口使用明确的允许列表，不因“兼容性”开放任意端口。</li>
<li><strong>目标白名单：</strong>对于图片、回调等固定场景，优先使用域名或服务标识白名单，而不是允许任意公网地址。</li>
<li><strong>统一出口代理：</strong>让业务容器不能直接访问网络，由代理集中执行域名、IP、端口、证书和流量大小策略。</li>
<li><strong>网络分段：</strong>应用运行区与管理区、数据库区、控制面隔离，拒绝不必要的东西向访问。</li>
<li><strong>重定向重新校验：</strong>建议默认关闭自动重定向；确有业务需求时限制跳转次数，并对每一跳重新校验。</li>
<li><strong>响应最小化：</strong>不向用户返回内部服务的完整响应，避免把状态码、响应头和错误堆栈原样暴露。</li>
<li><strong>可观测性：</strong>记录请求来源、规范化后的目标、最终连接地址、跳转次数、拒绝原因和响应大小，日志中注意脱敏凭据及敏感查询参数。</li>
</ul>

<h2>六、在测试环境中如何验证修复有效</h2>
<p>验证应围绕边界行为设计，而不是只用一个正常公网 URL 测试。可以在隔离网络中部署一个简单的模拟目标服务，并准备以下测试类别：</p>
<ul>
<li>合法的 HTTPS 公网测试域名，以及业务允许的固定端口。</li>
<li>回环、私有、链路本地、IPv6 和 IPv4 映射形式的地址。</li>
<li>带用户名密码的 URL、异常端口、空主机和解析失败的域名。</li>
<li>返回 3xx 的模拟服务，用于确认重定向目标会被再次校验。</li>
<li>解析到多个地址的域名，用于确认所有结果都经过检查。</li>
<li>慢响应和超大响应，用于确认超时、大小限制和资源释放逻辑。</li>
</ul>
<p>自动化测试应同时检查业务结果和审计日志。例如，拒绝请求时应返回统一错误，不泄露底层 DNS、连接库或内部地址；日志则应能够说明请求被哪条策略拒绝。修复后还要通过依赖升级、配置检查和网络连通性测试确认没有旁路路径。</p>

<h2>七、事件复盘应关注什么</h2>
<p>如果已经发现疑似 SSRF，不要只把某个参数加入黑名单。复盘时应回答四个问题：</p>
<ul>
<li>入口在哪里：是公开接口、后台功能、第三方回调，还是异步任务？</li>
<li>能力有多大：能访问哪些协议、端口、地址段，是否支持重定向和长响应？</li>
<li>影响如何确认：哪些内部服务收到过请求，是否出现异常认证、配置读取或管理操作？</li>
<li>为什么没有及时发现：是否缺少出口日志、异常请求告警、网络隔离或安全测试用例？</li>
</ul>
<p>处置阶段应先保留应用访问日志、代理日志、DNS 查询记录和相关时间窗口内的网络流量摘要，再按最小影响原则收紧出口策略、轮换可能暴露的凭据，并检查内部服务的认证和访问日志。对于无法确认影响范围的情况，应以时间线和证据链为基础进行判断，避免只依赖应用层返回结果。</p>

<h2>八、总结</h2>
<p>SSRF 防护的核心不是记住几个内网 IP，而是建立一条完整的控制链：正确解析 URL，限制协议和端口，检查所有 DNS 结果，控制重定向，在实际连接时固定经过校验的地址，并通过代理和网络分段降低单点失误的影响。</p>
<p>从审计角度看，最有效的方法是沿数据流确认“用户输入如何到达网络连接”，再结合代码、配置、容器网络和日志进行验证。只有把应用校验、出口控制、监控告警和事件响应放在一起，才能让修复从一次性补丁变成可持续的安全能力。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">294</guid><pubDate>Sun, 11 Oct 2026 23:36:09 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x770B; SpEL &#x8868;&#x8FBE;&#x5F0F;&#x6CE8;&#x5165;&#x7684;&#x8BC6;&#x522B;&#x3001;&#x9A8C;&#x8BC1;&#x4E0E;&#x4FEE;&#x590D;</title><link>https://www.hackhat.org/blogs/entry/293-%E4%BB%8E%E4%B8%80%E6%AC%A1%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E7%9C%8B-spel-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E7%9A%84%E8%AF%86%E5%88%AB%E3%80%81%E9%AA%8C%E8%AF%81%E4%B8%8E%E4%BF%AE%E5%A4%8D/</link><description><![CDATA[<h2>背景</h2><p>Spring 生态里 SpEL（Spring Expression Language）用得很广，常见于权限表达式、配置解析、缓存 Key 生成、规则引擎、动态模板等场景。它本身不是漏洞，问题通常出在“把外部可控输入当作表达式执行”。这类问题在代码审计中不算高频，但一旦出现在关键路径，影响面会比较大。</p><p>这篇文章整理一次比较典型的审计思路：如何从代码里识别 SpEL 注入风险，如何做安全验证，如何判断真实影响，以及如何修复。重点放在防守和工程落地，不讨论恶意利用链。</p><h2>问题场景</h2><p>某业务系统提供了一个“自定义规则”能力，管理员可以配置规则表达式，用于决定消息是否进入后续流程。最初设计时只考虑了内部人员使用，后来配置入口被接入到运营后台，并且部分字段可以由上游接口间接写入。</p><p>审计时看到类似代码：</p><pre><code>ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", user);
context.setVariable("order", order);

Expression expression = parser.parseExpression(ruleExpression);
Boolean matched = expression.getValue(context, Boolean.class);</code></pre><p>这里的核心风险点是：<code>ruleExpression</code> 如果来自不可信输入，就相当于允许外部用户控制被解析执行的表达式。即使业务上认为“只有后台能配”，也需要继续确认后台权限边界、接口鉴权、配置来源、导入功能、同步任务等是否可能扩大输入面。</p><h2>技术分析</h2><p>SpEL 的能力比较强，除了基础的属性访问、逻辑判断、集合操作外，还支持类型引用、方法调用、构造对象等能力。对于业务规则来说，通常只需要很小一部分表达式能力，例如：</p><pre><code>#user.level &gt;= 3 and #order.amount &gt; 100</code></pre><p>但如果使用默认解析器和默认上下文，表达式能力往往超过业务所需。安全审计时，可以从以下几个角度判断风险。</p><h2>一、输入是否可控</h2><ul><li>表达式是否来自 HTTP 请求参数、JSON 字段、Header、Cookie。</li><li>是否来自数据库配置，而配置是否可由用户、运营后台、导入任务修改。</li><li>是否来自消息队列、定时任务同步、第三方回调数据。</li><li>是否存在“低权限用户影响高权限规则执行”的路径。</li></ul><p>很多漏洞不是直接把请求参数传进 <code>parseExpression</code>，而是经过数据库、缓存、配置中心中转后再执行。审计时不要只看 Controller，要把数据流追到最终执行点。</p><h2>二、表达式执行上下文是否过宽</h2><p>以下写法需要重点关注：</p><pre><code>StandardEvaluationContext context = new StandardEvaluationContext(rootObject);</code></pre><p><code>StandardEvaluationContext</code> 功能完整，适合可信表达式场景。如果表达式来源不可信，更建议使用能力受限的上下文，例如 <code>SimpleEvaluationContext</code>，并显式限制可访问对象。</p><p>常见风险点包括：</p><ul><li>允许访问任意 Bean 或类型。</li><li>允许调用对象上的任意 public 方法。</li><li>上下文中放入了过多业务对象，例如用户、订单、环境配置、服务类实例。</li><li>表达式执行前未做白名单校验。</li></ul><h2>三、是否存在危险解析入口</h2><p>代码审计时可以先做关键词检索，定位 SpEL 使用点：</p><pre><code>grep -R "parseExpression" -n src/main/java

grep -R "SpelExpressionParser" -n src/main/java

grep -R "StandardEvaluationContext" -n src/main/java

grep -R "@Value" -n src/main/java</code></pre><p>如果项目较大，可以配合 semgrep 写一个粗略规则先扫一遍：</p><pre><code>rules:
  - id: java-spring-spel-parse-expression
    message: Review SpEL parseExpression usage with external input
    languages: [java]
    severity: WARNING
    patterns:
      - pattern: |
          $P.parseExpression($EXPR)</code></pre><p>这类规则只能帮助定位，不等于确认漏洞。确认时要继续分析 <code>$EXPR</code> 的来源、是否经过限制、上下文能力以及调用场景。</p><h2>安全验证方法</h2><p>验证时建议使用无副作用表达式，不要使用系统命令、网络请求、文件读写等破坏性动作。目标是证明“外部输入被当作表达式执行”，而不是追求攻击效果。</p><p>例如可以使用简单算术表达式验证：</p><pre><code>1 + 2</code></pre><p>如果业务期望表达式返回布尔值，可以用：</p><pre><code>1 + 2 == 3</code></pre><p>如果上下文中存在变量，可以验证变量访问能力：</p><pre><code>#user.level &gt;= 1</code></pre><p>验证时记录三类信息即可：</p><ul><li>输入点：表达式从哪里进入系统。</li><li>执行点：代码中哪个位置执行了 <code>parseExpression</code> 和 <code>getValue</code>。</li><li>影响范围：表达式能访问哪些变量、对象和方法。</li></ul><blockquote>建议在测试环境完成验证，并使用最小权限账号。生产环境只做代码路径确认和日志侧佐证，不建议直接投递测试表达式。</blockquote><h2>关键步骤</h2><h2>1. 梳理数据流</h2><p>从执行点向上追踪 <code>ruleExpression</code> 的来源。如果表达式来自数据库，需要继续找写入表字段的接口、后台页面、导入功能和同步任务。</p><pre><code>// 执行点
Expression expression = parser.parseExpression(ruleExpression);
Boolean result = expression.getValue(context, Boolean.class);</code></pre><p>实际审计中经常会遇到这种情况：开发认为字段只由管理员配置，但后台接口缺少细粒度权限，或者存在批量导入接口可以由普通运营角色调用。此时漏洞影响就不再局限于管理员误操作。</p><h2>2. 判断表达式能力边界</h2><p>重点看是否使用 <code>StandardEvaluationContext</code>，以及是否注册了额外能力：</p><pre><code>context.setBeanResolver(beanResolver);
context.setTypeLocator(typeLocator);
context.setMethodResolvers(methodResolvers);
context.setPropertyAccessors(propertyAccessors);</code></pre><p>这些扩展点本身是正常功能，但在不可信表达式场景下会显著扩大表达式能力。一般来说，业务规则只需要读取少量字段并做逻辑比较，不需要 Bean 访问、类型定位和任意方法调用。</p><h2>3. 使用白名单表达式模型</h2><p>修复时不要依赖黑名单过滤特殊字符串。黑名单很容易漏掉语法变体，也不利于长期维护。更稳妥的做法是收敛表达式语法和上下文能力。</p><p>示例：仅允许读取指定属性并做简单比较。</p><pre><code>SimpleEvaluationContext context = SimpleEvaluationContext
        .forReadOnlyDataBinding()
        .build();

Map&lt;String, Object&gt; root = new HashMap&lt;&gt;();
root.put("level", user.getLevel());
root.put("amount", order.getAmount());

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(ruleExpression);
Boolean matched = expression.getValue(context, root, Boolean.class);</code></pre><p>这里仍然需要限制 <code>ruleExpression</code> 的来源和语法。对于高风险业务，更建议不要直接暴露 SpEL，而是设计 DSL 或固定条件组合，例如字段、操作符、值三元组：</p><pre><code>{
  "field": "amount",
  "operator": "GT",
  "value": 100
}</code></pre><p>后端将其转换为受控判断逻辑，而不是执行用户输入的表达式。</p><h2>4. 增加表达式预校验</h2><p>如果历史原因必须继续兼容 SpEL，可以在解析前加入白名单校验，例如限制允许的变量名、操作符和字面量类型。示例思路：</p><ul><li>只允许 <code>#user.level</code>、<code>#order.amount</code> 等固定字段。</li><li>只允许比较运算、逻辑与或、括号。</li><li>禁止类型引用、构造对象、方法调用、Bean 引用。</li><li>限制表达式长度和嵌套深度。</li></ul><p>工程上可以将表达式解析为 AST 后检查节点类型，而不是简单正则。正则适合做第一层粗过滤，不适合作为唯一防线。</p><h2>5. 加日志和审计</h2><p>规则表达式属于高风险配置，建议记录完整变更审计：</p><ul><li>谁修改了表达式。</li><li>修改前后内容。</li><li>来源 IP 和操作入口。</li><li>审批单或变更原因。</li><li>规则命中后的关键业务动作。</li></ul><p>同时对异常表达式解析失败、频繁变更、非常规字符等行为做告警。很多时候，日志能帮助区分误配置、测试行为和真实风险。</p><h2>常见误区</h2><h2>误区一：只有管理员能配，所以没风险</h2><p>后台权限也需要分层。管理员账号可能被钓鱼、共享、误授权，后台接口也可能存在越权。只要表达式能力超过业务需要，就应该收敛。</p><h2>误区二：过滤几个关键字就够了</h2><p>表达式语言通常存在多种等价写法，黑名单很难覆盖完整。正确方向是白名单、最小权限上下文、减少暴露对象。</p><h2>误区三：测试没弹计算器，所以不是漏洞</h2><p>验证漏洞不应以破坏性效果为目标。只要能证明不可信输入被作为表达式执行，并且表达式能力超出预期，就应按风险处理。</p><h2>误区四：只修入口，不修执行点</h2><p>入口可能不止一个。修复应优先放在执行点附近，形成统一安全封装，避免后续新入口绕过。</p><h2>修复建议</h2><table><tr><th>措施</th><th>说明</th></tr><tr><td>替换为 DSL</td><td>优先推荐。将规则表达为字段、操作符和值，由后端受控执行。</td></tr><tr><td>使用 SimpleEvaluationContext</td><td>减少表达式能力，避免默认开放过多访问能力。</td></tr><tr><td>白名单字段</td><td>只暴露业务必需字段，不传入完整对象或服务实例。</td></tr><tr><td>限制语法</td><td>禁止方法调用、类型引用、构造对象、Bean 访问等非必要能力。</td></tr><tr><td>配置审计</td><td>记录表达式变更、审批、执行异常和命中情况。</td></tr><tr><td>统一封装</td><td>不要在业务代码里到处直接 new SpelExpressionParser，应封装为统一安全组件。</td></tr></table><h2>一个相对稳妥的封装思路</h2><p>可以将表达式执行集中到一个组件中，统一做长度限制、变量白名单、上下文收敛和异常处理。</p><pre><code>public class SafeRuleEvaluator {

    private static final int MAX_EXPR_LENGTH = 200;
    private final ExpressionParser parser = new SpelExpressionParser();
    private final SimpleEvaluationContext context = SimpleEvaluationContext
            .forReadOnlyDataBinding()
            .build();

    public boolean evaluate(String expression, Map&lt;String, Object&gt; variables) {
        if (expression == null || expression.length() &gt; MAX_EXPR_LENGTH) {
            throw new IllegalArgumentException("invalid expression length");
        }
        if (!isAllowedExpression(expression)) {
            throw new IllegalArgumentException("expression is not allowed");
        }
        Map&lt;String, Object&gt; safeVariables = filterVariables(variables);
        Expression exp = parser.parseExpression(expression);
        Boolean result = exp.getValue(context, safeVariables, Boolean.class);
        return Boolean.TRUE.equals(result);
    }

    private boolean isAllowedExpression(String expression) {
        // 示例：实际项目中建议基于 AST 做白名单校验
        return !expression.contains("T(")
                &amp;&amp; !expression.contains("new ")
                &amp;&amp; !expression.contains("@")
                &amp;&amp; !expression.contains(".class");
    }

    private Map&lt;String, Object&gt; filterVariables(Map&lt;String, Object&gt; variables) {
        Map&lt;String, Object&gt; safe = new HashMap&lt;&gt;();
        safe.put("level", variables.get("level"));
        safe.put("amount", variables.get("amount"));
        safe.put("region", variables.get("region"));
        return safe;
    }
}</code></pre><p>上面只是示例，不建议把字符串过滤当作最终方案。更可靠的方式是从产品形态上减少表达式自由度，例如改成固定条件配置。</p><h2>排查清单</h2><ul><li>项目中是否存在 <code>SpelExpressionParser</code>、<code>parseExpression</code>、<code>getValue</code>。</li><li>表达式参数是否可能来自用户输入、后台配置、导入文件、消息队列。</li><li>是否使用 <code>StandardEvaluationContext</code>。</li><li>上下文中是否放入完整业务对象、服务对象、环境对象。</li><li>是否注册 BeanResolver、TypeLocator、MethodResolver。</li><li>是否有表达式长度、语法、字段白名单限制。</li><li>是否有配置变更审计和异常告警。</li><li>是否存在多个入口写入同一表达式字段。</li></ul><h2>总结</h2><p>SpEL 注入的本质不是 Spring 有问题，而是把不可信输入交给了能力过强的表达式引擎。审计这类问题时，不要只盯着单个接口，要把“输入来源、存储中转、执行上下文、表达式能力、业务影响”串起来看。</p><p>修复上优先选择 DSL 或固定规则模型，其次才是受限 SpEL。无论采用哪种方案，都应遵循最小能力原则：只暴露必要字段，只允许必要语法，只在必要场景执行，并留下可追溯的配置审计。这样即使后续业务扩展，也不容易把一个规则功能演变成高风险执行入口。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">293</guid><pubDate>Sun, 11 Oct 2026 23:32:11 +0000</pubDate></item><item><title>&#x4ECE;&#x8BF7;&#x6C42;&#x94FE;&#x8DEF;&#x5230;&#x8BC1;&#x636E;&#x95ED;&#x73AF;&#xFF1A;&#x4E00;&#x6B21; SSRF &#x98CE;&#x9669;&#x7684;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E0E;&#x4FEE;&#x590D;&#x5B9E;&#x8DF5;</title><link>https://www.hackhat.org/blogs/entry/292-%E4%BB%8E%E8%AF%B7%E6%B1%82%E9%93%BE%E8%B7%AF%E5%88%B0%E8%AF%81%E6%8D%AE%E9%97%AD%E7%8E%AF%EF%BC%9A%E4%B8%80%E6%AC%A1-ssrf-%E9%A3%8E%E9%99%A9%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%8E%E4%BF%AE%E5%A4%8D%E5%AE%9E%E8%B7%B5/</link><description><![CDATA[<p>SSRF（服务端请求伪造）在实际项目中并不总是以一个明显的“URL 参数直接传给 HTTP 客户端”形式出现。更常见的情况是：业务允许导入远程图片、解析 Webhook、抓取网页标题，或者通过内部服务生成预览；随着代码分层和功能迭代，用户输入可能经过多次拼接、解码和重定向，最终形成一条不容易被发现的服务端请求链路。</p><p>这类问题的风险也不能只看“能不能访问内网”。真正需要评估的是请求发起位置、解析和连接过程、重定向行为、响应处理方式，以及云环境中的凭证和管理接口是否暴露。本文以一次典型的代码审计为例，整理一套偏工程化的发现、复现、修复和验证方法。示例均使用本地测试环境，不涉及对第三方系统的探测。</p><h2>一、问题场景：图片预览功能为何容易出问题</h2><p>某内容系统提供远程图片预览功能，接口接收一个图片地址，服务端下载图片后生成缩略图。初始实现大致如下：</p><pre><code>def fetch_image(url: str) -&gt; bytes:
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    return response.content</code></pre><p>从业务角度看，这段代码很直观，但安全边界几乎没有建立：</p><ul><li>协议没有限制，调用方可能尝试非 HTTP 协议处理器。</li><li>域名解析结果没有校验，域名可能解析到本机、内网或链路本地地址。</li><li>即使首次解析结果安全，连接前再次解析时也可能发生地址变化。</li><li>默认跟随重定向，初始地址安全并不代表最终地址安全。</li><li>没有限制响应大小，可能造成内存或磁盘资源耗尽。</li><li>如果异常信息直接返回给前端，可能泄露内部地址、连接细节或响应内容。</li></ul><h2>二、审计重点：不要只搜索一个函数</h2><p>审计 SSRF 时，建议从“输入源—规范化—请求构造—网络连接—响应处理”五个环节建立数据流，而不是只搜索 requests.get 或 HttpClient.execute。</p><h3>1. 识别外部可控输入</h3><p>重点检查以下输入来源：</p><ul><li>JSON、表单、GraphQL 和 RPC 请求中的 URL、回调地址、图片地址。</li><li>导入文件中的远程引用，例如 Markdown、RSS 或配置文件。</li><li>数据库中由用户创建的 Webhook、通知地址和头像地址。</li><li>消息队列、定时任务或管理后台中的抓取任务参数。</li></ul><p>一旦发现 URL 被保存到数据库，不能认为风险已经结束。持久化字段往往会在后续异步任务中被重新取出并请求，审计范围应覆盖同步接口、队列消费者和定时任务。</p><h3>2. 追踪请求封装层</h3><p>实际项目通常有统一的网络访问封装，例如 download、fetch、proxy、preview 等方法。建议沿调用链向下追踪，重点确认：</p><ul><li>是否允许调用方覆盖代理、请求头和超时时间。</li><li>是否对重定向后的每一跳重新执行安全校验。</li><li>是否先解析域名再建立连接，还是由底层库自动解析。</li><li>是否把响应内容交给 XML、图片、压缩包等二次解析器。</li><li>是否将响应头、状态码和异常原文返回给外部调用方。</li></ul><h3>3. 关注“看起来不是 URL”的输入</h3><p>有些危险输入不一定直接叫 url。例如 host、endpoint、callback、source、avatar、image、feed、webhook 等字段，经过拼接后可能构成完整请求地址。对这类字段，应该结合数据流和实际调用点判断，而不能只依赖变量命名。</p><h2>三、地址校验的关键：校验最终连接目标，而不是只校验字符串</h2><p>常见的错误做法是使用字符串前缀判断，例如判断地址是否以某个域名开头。这种方式无法可靠处理用户信息、端口、大小写、尾随点、IPv6、域名解析和重定向等情况。</p><p>较稳妥的策略通常包括以下步骤：</p><ul><li>只接受明确允许的协议，例如 http 和 https。</li><li>解析 URL，要求存在主机名，拒绝用户名、密码和不必要的自定义端口。</li><li>对主机名做规范化处理，避免大小写、尾随点和国际化域名造成判断差异。</li><li>解析所有 A 和 AAAA 记录，对每个地址执行内网、回环、链路本地、组播和保留地址检查。</li><li>实际连接时绑定经过校验的 IP，同时保留正确的 Host 和 TLS SNI，避免重新解析到其他地址。</li><li>禁用或接管自动重定向，并对每次跳转重新执行完整校验。</li></ul><p>下面是一个用于本地服务的简化校验示例。它表达的是安全边界和测试思路，生产环境还需要结合网络库的连接绑定能力实现完整的 DNS 重绑定防护。</p><pre><code>from ipaddress import ip_address
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {"http", "https"}


def is_public_address(value: str) -&gt; bool:
    address = ip_address(value)
    return not (
        address.is_private
        or address.is_loopback
        or address.is_link_local
        or address.is_multicast
        or address.is_reserved
        or address.is_unspecified
    )


def validate_target(raw_url: str) -&gt; tuple[str, int]:
    parsed = urlparse(raw_url)
    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        raise ValueError("unsupported scheme")
    if not parsed.hostname or parsed.username or parsed.password:
        raise ValueError("invalid authority")

    host = parsed.hostname.rstrip(".").lower()
    port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
    addresses = {item[4][0] for item in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)}
    if not addresses or not all(is_public_address(item) for item in addresses):
        raise ValueError("target address is not allowed")
    return host, port</code></pre><p>这里有一个容易被忽略的细节：只检查 DNS 查询结果仍然不一定足够。若 HTTP 客户端稍后根据域名重新解析，攻击者控制的 DNS 服务可能返回不同地址。因此生产实现应选择支持“预解析地址并绑定连接”的客户端，或者在网络层增加出口代理和目的地址策略，避免应用层单独承担全部风险。</p><h2>四、重定向、解析差异与协议处理</h2><h3>1. 重定向必须逐跳校验</h3><p>很多 HTTP 客户端默认自动跟随 301、302、303 和 307。若第一跳是公开地址，响应再跳转到受限地址，单次入口校验就会失效。建议默认关闭自动重定向；如果业务确实需要跟随，应限制最大跳数，并对 Location 解析、协议、主机、端口和解析结果逐跳校验。</p><pre><code>def fetch_with_policy(url: str, session) -&gt; bytes:
    current = url
    for _ in range(3):
        validate_target(current)
        response = session.get(
            current,
            allow_redirects=False,
            timeout=(3, 5),
            stream=True,
        )
        if response.status_code not in {301, 302, 303, 307, 308}:
            return read_limited_body(response, limit=5 * 1024 * 1024)

        location = response.headers.get("Location")
        if not location:
            raise ValueError("redirect location is empty")
        current = urljoin(current, location)

    raise ValueError("too many redirects")</code></pre><p>上面的示例省略了导入和响应体读取函数，重点是控制重定向边界。还需要注意：不同语言和库对 URL 解析、反斜杠、编码字符、空白字符以及 IPv6 表示法的处理可能不同，校验器与实际 HTTP 客户端最好使用同一套规范化逻辑，避免“校验看到的地址”和“连接实际使用的地址”不一致。</p><h3>2. 不要把非 HTTP 协议交给通用请求组件</h3><p>如果业务只需要获取网页或图片，就不应让通用组件处理 file、ftp、gopher 等协议。某些库还支持自定义协议处理器，或者会根据输入自动选择不同的适配器。限制协议只是第一步，最好在依赖配置和代码层同时确认不加载无关协议处理能力。</p><h2>五、响应处理同样是攻击面</h2><p>SSRF 风险不仅在于“请求发出去了什么”，也在于“服务端拿回来后做了什么”。审计时应继续检查以下链路：</p><ul><li>响应是否被完整读入内存，是否有最大响应体限制。</li><li>Content-Type 是否可信，是否会根据文件内容自动识别类型。</li><li>图片、XML、PDF、压缩包等解析器是否存在二次解析风险。</li><li>响应是否被原样返回，是否可能成为内部服务内容代理。</li><li>是否把上游响应头中的 Location、Set-Cookie 或缓存字段传递给客户端。</li></ul><p>一个相对稳妥的下载流程应采用流式读取、大小上限和内容类型白名单。例如图片预览只允许 image/jpeg、image/png 等有限类型，并在服务端重新编码后再输出，不直接返回上游字节流。</p><pre><code>def read_limited_body(response, limit: int) -&gt; bytes:
    total = 0
    chunks = []
    for chunk in response.iter_content(chunk_size=8192):
        if not chunk:
            continue
        total += len(chunk)
        if total &gt; limit:
            raise ValueError("response body is too large")
        chunks.append(chunk)
    return b"".join(chunks)</code></pre><h2>六、网络层防护：不要只依赖应用代码</h2><p>应用层校验适合表达业务允许访问的范围，但无法替代网络隔离。生产环境中建议同时落实以下措施：</p><ul><li>将抓取服务放在独立的网络分区，默认禁止访问数据库、缓存、消息队列和管理平面。</li><li>通过出口代理统一执行域名、端口、目的地址和审计策略。</li><li>限制应用运行身份的云平台权限，不让普通业务实例读取高价值实例凭证。</li><li>在防火墙或安全组层面拒绝回环、链路本地、私有网段和云管理地址。</li><li>对外联请求记录请求任务、规范化主机、解析地址、最终状态和失败原因，但避免记录敏感查询参数。</li></ul><p>日志中的地址信息应经过脱敏和权限控制。安全团队需要能够根据任务 ID 还原完整请求链路，同时避免把内部拓扑、凭证参数或上游响应直接写入普通业务日志。</p><h2>七、验证方法：建立可重复的本地测试</h2><p>修复后不要只通过一次接口调用判断问题已经解决。可以在本地搭建几个隔离测试服务，覆盖不同的边界条件：</p><ul><li>公开地址直接访问，应符合业务预期。</li><li>解析到回环、私有网段、链路本地和 IPv6 保留地址时，应在连接前失败。</li><li>公开地址重定向到受限地址时，应在第二跳被拒绝。</li><li>多次解析返回不同地址时，应保持连接目标与校验结果一致。</li><li>响应体超过限制时，应中止读取并释放连接。</li><li>非允许协议、缺失主机、带用户信息或异常端口的 URL，应统一拒绝。</li></ul><p>测试重点是验证“是否建立了连接”，而不只是观察接口返回码。可以在隔离网络中使用一个仅用于测试的监听服务记录连接来源和 Host，不要将测试请求指向真实内网系统。</p><pre><code>def test_redirect_to_private_target_is_rejected(client, redirect_server):
    public_url = redirect_server.url_for("
    result = client.preview({"url": public_url})
    assert result.status_code == 400
    assert redirect_server.connection_count == 1
</code></pre><p>除单元测试外，还应加入静态检查和集成测试。例如在代码审查中搜索未经统一封装的网络请求调用：</p><pre><code>rg 'requests\.(get|post|request)|urlopen|HttpClient|WebClient' src/</code></pre><p>搜索结果不能直接等同于漏洞，但可以帮助发现绕过统一安全组件的新增代码。对于 Java、Go、Node.js 等技术栈，应根据项目实际客户端补充对应规则，并把检查接入代码审查或持续集成流程。</p><h2>八、常见修复误区</h2><ul><li>只做字符串黑名单：黑名单容易遗漏地址表示形式，也无法处理解析和重定向。</li><li>只禁止常见私网段：还应考虑回环、链路本地、组播、保留地址和 IPv6。</li><li>只校验第一次请求：重定向和多阶段抓取会产生新的目标。</li><li>只在前端校验：前端校验可以改善交互，但不能作为服务端安全边界。</li><li>修复后仍允许任意代理设置：攻击者可能通过代理改变实际请求路径。</li><li>错误信息过于详细：应对外返回稳定的业务错误码，对内记录足够的诊断信息。</li></ul><h2>九、结论</h2><p>SSRF 的治理核心不是增加一个 URL 黑名单，而是把“服务端是否允许访问某个网络目标”变成明确、可审计、可测试的策略。一次完整的修复应同时覆盖输入源追踪、协议限制、主机解析、连接绑定、重定向控制、响应大小、网络隔离和日志审计。</p><p>从代码审计角度看，最有价值的证据不是某个请求函数的存在，而是一条能够闭合的数据流：外部输入如何进入任务，经过哪些规范化和校验，最终连接到哪个地址，响应又被哪些组件继续处理。只要围绕这条链路建立测试和防护，类似的远程抓取、Webhook、文件导入和预览功能就能复用同一套安全基线，减少问题在不同业务模块中反复出现。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">292</guid><pubDate>Sun, 11 Oct 2026 23:30:13 +0000</pubDate></item><item><title>&#x4ECE;&#x8BEF;&#x62A5;&#x5230;&#x95ED;&#x73AF;&#xFF1A;&#x4E00;&#x6B21; SSRF &#x98CE;&#x9669;&#x7684;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E0E;&#x9632;&#x62A4;&#x5B9E;&#x8DF5;</title><link>https://www.hackhat.org/blogs/entry/291-%E4%BB%8E%E8%AF%AF%E6%8A%A5%E5%88%B0%E9%97%AD%E7%8E%AF%EF%BC%9A%E4%B8%80%E6%AC%A1-ssrf-%E9%A3%8E%E9%99%A9%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/</link><description><![CDATA[<p>SSRF（服务端请求伪造）经常出现在“根据 URL 抓取内容”“图片远程转存”“Webhook 测试”“网页截图”等功能中。它的危险性不只在于访问内网地址，更在于应用自身拥有网络权限、身份凭据和信任关系，一旦请求边界设计不清晰，就可能把一个普通的业务接口变成内部网络访问代理。</p><p>本文以一次典型代码审计为背景，讨论如何判断 SSRF 是否成立、如何在不触碰真实敏感系统的前提下完成验证，以及如何从 URL 解析、重定向、DNS 解析和出口控制多个层面建立防护闭环。</p><h2>一、问题场景：一个看似普通的远程抓取接口</h2><p>目标功能是让用户提交一个图片地址，服务端下载图片并保存到对象存储。初始代码逻辑大致如下：</p><pre><code>public byte[] fetch(String url) throws IOException {
    URL target = new URL(url);
    HttpURLConnection connection = (HttpURLConnection) target.openConnection();
    connection.setConnectTimeout(3000);
    connection.setReadTimeout(5000);
    connection.setInstanceFollowRedirects(true);
    return connection.getInputStream().readAllBytes();
}</code></pre><p>这段代码的业务意图很明确，但安全边界并不完整：</p><ul><li>允许用户完全控制目标 URL。</li><li>没有限制协议，只是通过 URL 类自动选择处理方式。</li><li>没有校验目标 IP 是否属于内网、回环、链路本地或保留地址。</li><li>默认跟随重定向，初始地址合法并不代表最终地址合法。</li><li>没有限制响应大小，可能引入资源消耗问题。</li><li>应用所在网络环境可能可以访问管理面板、服务发现接口或内部调试端点。</li></ul><p>需要注意的是，SSRF 是否能够造成实际影响，取决于应用部署位置、网络分段、身份权限和出口策略。代码存在缺陷不等于已经证明可以访问某个敏感系统，审计结论应区分“代码级风险”和“环境级影响”。</p><h2>二、审计时先确认请求链路</h2><p>分析 SSRF 时，不要一开始就围绕某个特殊地址构造测试。更稳妥的做法是先梳理完整请求链路：</p><ul><li>用户输入从哪里进入，是否经过 URL 解码、JSON 反序列化或模板拼接。</li><li>最终使用的是哪一种 HTTP 客户端，是否自动跟随重定向。</li><li>域名解析发生在应用进程、代理还是网关。</li><li>是否存在代理环境变量，例如 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。</li><li>响应内容是否会回显给用户，还是只保存到文件或对象存储。</li><li>请求是否携带了默认 Cookie、Authorization 或云平台身份凭据。</li></ul><p>可以在测试环境中增加结构化日志，但不应记录完整敏感 URL 或认证头。推荐记录请求任务编号、目标主机的脱敏值、解析后的地址类别、最终状态码、响应大小和耗时。例如：</p><pre><code>{
  "event": "outbound_fetch",
  "request_id": "8f1c...",
  "host": "example.invalid",
  "resolved_ip_class": "public",
  "redirect_count": 0,
  "status": 200,
  "bytes": 18432,
  "duration_ms": 421
}</code></pre><h2>三、验证方法：只使用自建或授权测试目标</h2><p>在授权测试环境中，可以准备一个外部测试站点和一个本地测试站点，用于观察请求是否从服务端发出。测试站点只返回固定文本，不包含任何敏感数据。验证重点不是读取内部信息，而是确认以下事实：</p><ul><li>请求是否确实由服务端发起，而不是浏览器直接访问。</li><li>服务端是否能访问仅对本机或内网开放的测试端口。</li><li>遇到 3xx 响应时，客户端是否继续访问新的地址。</li><li>DNS 解析结果变化后，客户端是否重新校验目标地址。</li><li>响应大小、连接时间和重定向次数是否受到限制。</li></ul><p>一个安全的本地验证服务可以只监听测试机的回环接口，并返回固定内容：</p><pre><code>from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = b"authorized-ssrf-test"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        print(fmt % args)

HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()</code></pre><p>测试完成后，应删除临时服务并核对访问日志。不要把生产环境的管理接口、容器元数据服务、数据库端口或其他内部系统作为验证目标。</p><h2>四、核心风险一：URL 解析与 IP 校验不一致</h2><p>很多防护只做了字符串判断，例如禁止 URL 中出现 localhost 或某个内网网段。这种方式容易受到不同解析器行为差异的影响。实际审计时应关注以下问题：</p><ul><li>是否只允许 http 和 https，是否会接受 file、ftp 或其他协议。</li><li>主机名是否经过标准化处理，包括大小写、尾部点和国际化域名。</li><li>校验的是原始域名，还是实际 DNS 解析后的每一个地址。</li><li>IPv4、IPv6、IPv4 映射 IPv6 的处理是否一致。</li><li>是否错误地把带用户名信息的 URL 当成普通主机名解析。</li></ul><p>推荐采用“解析 URL—规范化主机名—解析全部地址—逐个判断地址类别—再建立连接”的顺序，而不是先用字符串黑名单过滤。</p><p>地址判断至少应覆盖回环地址、私有地址、链路本地地址、未指定地址、组播地址、文档保留地址以及企业网络明确禁止的网段。对于 IPv6，不能只用 IPv4 的正则表达式处理。</p><h2>五、核心风险二：DNS 解析与连接之间的时间差</h2><p>即使代码先解析域名并确认结果属于公网，也可能在真正建立连接时再次解析，导致检查结果与连接目标不一致。这类问题本质上是“检查时”和“使用时”之间存在时间窗口。</p><p>防护思路包括：</p><ul><li>使用经过安全封装的 HTTP 客户端，在连接阶段固定已校验的地址。</li><li>对每次连接和每次重定向重新执行地址校验。</li><li>对域名解析结果设置合理缓存时间，避免无限信任长期结果。</li><li>在网络层配置出口代理，由代理统一执行目标地址与端口策略。</li><li>不要把 DNS 校验当成唯一防线，必须配合防火墙和网络分段。</li></ul><p>如果业务允许，最简单可靠的方案通常是维护目标域名白名单，并在网关层只允许访问经过审核的目标，而不是试图覆盖所有复杂的地址解析边界。</p><h2>六、核心风险三：重定向绕过初始校验</h2><p>重定向是实际项目中最容易遗漏的路径。应用可能先校验了一个公开站点，但该站点返回 302，将客户端引向另一个未授权地址。如果客户端自动跟随，初始校验就失去了意义。</p><p>有两种常见处理方式：</p><ul><li>默认关闭自动重定向，由业务代码读取 Location 后重新解析、校验并决定是否继续。</li><li>如果必须自动跟随，则限制重定向次数，并确保客户端在每一跳都执行协议、域名、端口和 IP 校验。</li></ul><p>伪代码示例如下：</p><pre><code>response = request_once(url)
for i in range(MAX_REDIRECTS):
    if response.status not in REDIRECT_STATUS:
        break

    next_url = resolve_location(url, response.headers["Location"])
    target = parse_and_normalize(next_url)
    assert target.scheme in {"http", "https"}
    assert target.port in {80, 443}

    addresses = resolve_all(target.hostname)
    if any(is_blocked_address(addr) for addr in addresses):
        raise PolicyError("redirect target is not allowed")

    url = target.to_string()
    response = request_once(url)</code></pre><p>这里的关键点是：重定向目标必须被当成新的用户输入处理，而不是当成可信的服务器返回值。</p><h2>七、建立一套可落地的防护策略</h2><h3>1. 协议与端口白名单</h3><p>仅允许业务真正需要的协议，通常是 HTTPS，必要时再开放 HTTP。端口应采用白名单，而不是简单禁止几个常见端口。若图片转存只需要访问公网 443 端口，就没有必要允许任意端口。</p><h3>2. 目标域名白名单</h3><p>对于合作方回调、固定图床或文档系统，优先维护精确域名白名单。白名单匹配应避免简单的后缀判断，例如允许 example.com 时，不应顺带允许 evil-example.com。可以采用规范化后完全匹配，或明确的子域名边界匹配。</p><h3>3. 禁止访问敏感地址类别</h3><p>在应用层和出口代理层同时阻断回环、私有、链路本地、组播、未指定及企业内部保留网段。应用层负责业务策略，网络层负责最后一道边界，两者不能互相替代。</p><h3>4. 限制请求规模</h3><p>设置连接超时、读取超时、最大响应大小、最大重定向次数和最大下载时间。读取响应时不要无条件调用 readAllBytes，应使用带上限的流式读取：</p><pre><code>MAX_BYTES = 5 * 1024 * 1024
received = 0
chunks = []

while True:
    chunk = stream.read(8192)
    if not chunk:
        break
    received += len(chunk)
    if received &gt; MAX_BYTES:
        raise SizeLimitError("response too large")
    chunks.append(chunk)</n
body = b"".join(chunks)</code></pre><h3>5. 限制出站身份</h3><p>运行抓取任务的进程不应继承无关的云平台凭据、内部服务令牌或宿主机管理权限。生产环境建议使用独立的服务账户、独立网络段和最小化的出口策略，并对出站请求建立审计记录。</p><h2>八、代码审计中的常见误区</h2><ul><li><strong>只看控制器，不看底层客户端。</strong> 真正的重定向、代理和 DNS 行为通常发生在 HTTP 客户端配置中。</li><li><strong>只验证“能否访问内网”。</strong> 即使无法直接访问内网，任意外连也可能造成数据外传、内部探测或资源消耗。</li><li><strong>只做域名黑名单。</strong> 黑名单难以覆盖解析差异和新增内部网段，优先考虑白名单与网络出口控制。</li><li><strong>把 SSRF 当成单一漏洞修复。</strong> URL 解析、文件类型校验、响应大小、凭据隔离和日志审计都属于同一条请求链路。</li><li><strong>测试时直接访问真实敏感服务。</strong> 这既可能造成越权访问，也会污染事件记录。应优先使用自建的无害验证服务。</li></ul><h2>九、修复后的验证清单</h2><p>修复完成后，建议在测试环境执行一组固定回归用例，并将结果纳入自动化测试：</p><table><tr><th>测试项</th><th>预期结果</th></tr><tr><td>允许的 HTTPS 公网域名</td><td>请求成功，记录目标和响应摘要</td></tr><tr><td>不允许的协议</td><td>在发起连接前拒绝</td></tr><tr><td>回环或私有地址</td><td>拒绝，日志记录为策略阻断</td></tr><tr><td>解析到多个地址，其中一个被禁止</td><td>整体拒绝</td></tr><tr><td>重定向到未授权域名</td><td>重新校验后拒绝</td></tr><tr><td>超大响应或长时间无响应</td><td>触发大小或时间限制</td></tr><tr><td>异常 URL 编码和大小写形式</td><td>规范化后执行同等策略</td></tr></table><p>此外，还应检查监控是否能够区分业务失败与安全策略阻断，避免大量策略拒绝被普通网络错误淹没。</p><h2>十、总结</h2><p>SSRF 防护的核心不是记住几组特殊地址，而是建立清晰的出站请求边界：谁可以发起请求、允许访问什么协议和目标、解析结果是否可信、重定向是否重新校验、响应资源是否受限、网络出口是否有第二道控制。</p><p>在代码审计中，建议沿着“输入—解析—DNS—连接—重定向—响应—存储”的完整链路检查，而不是只搜索某个函数名。对于无法完全控制目标的通用抓取功能，优先采用域名白名单、专用出口代理、最小权限运行和完整审计日志。这样即使某一层校验出现遗漏，也不会轻易演变成对内部系统的任意访问。</p><blockquote>安全验证应以最小影响为原则：使用授权目标、固定测试数据和可回收环境证明问题；修复则应依赖应用策略与网络边界的组合，而不是单个字符串过滤。</blockquote>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-cloud.svg" alt="服务器与云环境安全示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">服务器、云资产与权限边界梳理示意</figcaption></figure>]]></description><guid isPermaLink="false">291</guid><pubDate>Sun, 11 Oct 2026 23:25:52 +0000</pubDate></item><item><title>&#x4E00;&#x6B21;&#x9762;&#x5411;&#x5185;&#x7F51; Web &#x8D44;&#x4EA7;&#x7684;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E0E;&#x98CE;&#x9669;&#x6536;&#x655B;&#x5B9E;&#x8DF5;</title><link>https://www.hackhat.org/blogs/entry/290-%E4%B8%80%E6%AC%A1%E9%9D%A2%E5%90%91%E5%86%85%E7%BD%91-web-%E8%B5%84%E4%BA%A7%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%8E%E9%A3%8E%E9%99%A9%E6%94%B6%E6%95%9B%E5%AE%9E%E8%B7%B5/</link><description><![CDATA[<h2>背景</h2><p>很多内网 Web 系统上线时间早、业务依赖重，平时很少有人系统性审计。真正出问题时，常见情况不是单点漏洞特别复杂，而是几个普通风险叠加：老旧框架、调试接口遗留、上传目录可访问、权限校验分散、日志缺失。本文整理一次偏防守视角的代码审计与风险收敛方法，重点放在如何发现问题、验证风险边界、推动修复，不涉及未授权攻击或恶意利用。</p><p>场景假设是企业内部自研 Web 系统，技术栈为 Java Spring MVC / Spring Boot 混合，前端有少量老 JSP，部署在 Linux + Nginx + Tomcat 环境。目标不是“打穿系统”，而是通过代码审计和受控验证，把可被利用的缺陷收敛到可修复、可复测的清单。</p><h2>问题与场景</h2><p>这类系统常见的问题并不新，但因为业务迭代多年，风险点往往藏在边角位置：</p><ul><li>接口权限校验不一致：部分 Controller 依赖前端菜单控制，没有后端鉴权。</li><li>文件上传处理粗糙：只校验后缀或 Content-Type，上传目录又能被直接访问。</li><li>SQL 拼接遗留：新模块使用 ORM，老模块仍有字符串拼接 SQL。</li><li>配置暴露：测试配置、默认口令、调试开关混入生产分支。</li><li>日志与审计不足：关键操作没有记录操作者、对象、结果，事件复盘困难。</li></ul><p>审计时建议先不要急着跑工具扫一遍就结束。工具能帮忙发现一部分明显问题，但对业务权限、对象越权、流程缺陷的覆盖有限。更稳妥的方式是先梳理入口，再按风险类型分层排查。</p><h2>审计前的准备</h2><p>代码审计前我通常会先整理三类信息：部署拓扑、路由入口、敏感功能。这样后续验证会更有边界，不容易误伤业务。</p><ul><li>部署拓扑：Nginx 转发规则、Tomcat context、静态目录、上传目录、定时任务。</li><li>路由入口：Controller、Filter、Interceptor、Servlet、JSP 直连页面。</li><li>敏感功能：登录、用户管理、角色权限、文件上传下载、导入导出、报表查询、系统配置。</li></ul><p>可以先用简单命令对代码结构做一个粗略索引：</p><pre><code>find . -type f \( -name "*.java" -o -name "*.jsp" -o -name "*.xml" -o -name "*.properties" -o -name "*.yml" \) | wc -l

grep -RIn "@RequestMapping\|@GetMapping\|@PostMapping" src/ | head -n 50

grep -RIn "upload\|download\|export\|import\|FileInputStream\|FileOutputStream" src/</code></pre><p>如果是 Spring Boot 项目，可以重点关注以下位置：</p><ul><li>安全配置：SecurityConfig、WebMvcConfigurer、Interceptor 注册逻辑。</li><li>统一异常：是否吞掉异常，是否把堆栈直接返回前端。</li><li>配置文件：application-prod.yml、bootstrap.yml、logback 配置。</li><li>依赖版本：pom.xml / build.gradle 中是否存在长期未升级组件。</li></ul><h2>技术分析：几个高频风险点</h2><h2>1. 权限校验分散导致的接口越权</h2><p>很多老系统会在页面层控制菜单显示，但后端接口本身没有强制鉴权。审计时不能只看登录拦截器是否存在，还要看“登录态”和“操作权限”是否分开校验。</p><p>一个比较危险的模式是只判断 session 中是否存在用户：</p><pre><code>if (session.getAttribute("user") == null) {
    return "redirect:/login";
}</code></pre><p>这种做法只能说明用户已登录，不能说明用户有权限访问某个接口。更合理的方式是让权限校验集中在 Filter、Interceptor 或安全框架中，并且权限点绑定到后端路由，而不是只绑定前端菜单。</p><p>排查时可以搜索：</p><pre><code>grep -RIn "getAttribute(\"user\"\)\|hasPermission\|checkPermission\|@PreAuthorize\|@RequiresPermissions" src/</code></pre><p>如果同一个项目里既有 @PreAuthorize，又有手写 session 判断，还存在完全没有注解的 Controller，就要重点抽样验证。验证时应在授权测试环境中使用不同角色账号对比访问结果，避免对生产环境做破坏性操作。</p><h2>2. 文件上传与可访问目录叠加</h2><p>上传漏洞的风险不一定来自“上传后执行”，也可能来自敏感文件落地、覆盖文件、路径穿越、恶意内容被二次解析等。审计重点包括：</p><ul><li>是否只依赖文件名后缀判断。</li><li>是否使用用户传入的原始文件名直接落盘。</li><li>上传目录是否在 Web 根目录下。</li><li>下载接口是否存在任意文件读取风险。</li><li>是否对文件大小、类型、数量做限制。</li></ul><p>常见的不安全写法如下：</p><pre><code>String fileName = multipartFile.getOriginalFilename();
File dest = new File(uploadPath + "/" + fileName);
multipartFile.transferTo(dest);</code></pre><p>至少应做这些处理：生成服务端文件名、白名单扩展名、限制大小、校验真实文件类型、目录规范化、上传目录禁止执行、下载时按文件 ID 映射真实路径。</p><pre><code>Path baseDir = Paths.get(uploadPath).toAbsolutePath().normalize();
String safeName = UUID.randomUUID().toString() + ".pdf";
Path target = baseDir.resolve(safeName).normalize();

if (!target.startsWith(baseDir)) {
    throw new IllegalArgumentException("invalid path");
}</code></pre><p>Nginx 层也建议明确限制上传目录的执行能力，不要让上传目录被脚本引擎处理。示例仅表达思路，具体规则要结合实际路径调整：</p><pre><code>location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    default_type application/octet-stream;
    add_header X-Content-Type-Options nosniff;
}</code></pre><h2>3. SQL 拼接与动态查询</h2><p>老系统里最常见的是“新代码用 MyBatis 参数绑定，老代码直接拼接条件”。尤其是报表、导出、综合查询页面，字段多、条件多，很容易出现拼接 SQL。</p><pre><code>String sql = "select * from orders where 1=1";
if (StringUtils.isNotBlank(keyword)) {
    sql += " and customer_name like '%" + keyword + "%'";
}</code></pre><p>审计时可以搜索以下关键词：</p><pre><code>grep -RIn "Statement\|createQuery\|createNativeQuery\|\$\{\|order by \|like '%" src/</code></pre><p>需要注意，MyBatis 中 <code>${}</code> 不等于 <code>#{}</code>。前者是字符串替换，后者是参数绑定。对于排序字段这类无法直接绑定的场景，应使用白名单映射，而不是把用户输入直接拼进 SQL。</p><pre><code>Map&lt;String, String&gt; allowSort = new HashMap&lt;&gt;();
allowSort.put("time", "create_time");
allowSort.put("amount", "total_amount");

String sortColumn = allowSort.getOrDefault(req.getSort(), "create_time");</code></pre><h2>4. 反序列化与表达式注入的边界排查</h2><p>Java 项目如果引入过老版本组件，反序列化和表达式注入需要单独看。这里不建议在业务环境做危险 payload 验证，更适合通过依赖分析、调用链确认、受控单元测试来判断风险。</p><p>可以先看依赖树：</p><pre><code>mvn dependency:tree | egrep "commons-collections|fastjson|jackson-databind|xstream|shiro|spring-expression"</code></pre><p>关注点包括：</p><ul><li>是否存在历史高危版本组件。</li><li>是否有对外暴露的反序列化入口，例如直接接收 ObjectInputStream、XMLDecoder。</li><li>是否启用了危险的类型自动识别能力。</li><li>表达式是否包含用户可控输入。</li></ul><p>判断这类风险时，不要只凭“存在某个依赖”就下结论。更准确的结论应包含：组件版本、是否可达、入口在哪里、用户输入能否控制关键参数、现有防护是否生效。</p><h2>5. 配置与日志问题</h2><p>配置泄露和日志缺失经常被低估。比如测试环境回调地址、默认管理口令、对象存储密钥、数据库连接串被提交到仓库。审计时可以对配置文件做一次敏感信息扫描：</p><pre><code>grep -RIn "password\|passwd\|secret\|accessKey\|token\|jdbc:\|redis" . \
  --exclude-dir=target --exclude-dir=.git</code></pre><p>日志方面，建议至少覆盖这些事件：</p><ul><li>登录成功、失败、锁定、退出。</li><li>新增、删除、修改用户与角色。</li><li>导入、导出、下载敏感数据。</li><li>系统配置变更。</li><li>权限拒绝与异常访问。</li></ul><p>日志要避免记录明文密码、完整 token、身份证号等敏感数据。更推荐记录脱敏后的对象标识、操作者 ID、来源 IP、请求 ID、结果状态，便于后续排查。</p><h2>关键步骤：从发现到修复闭环</h2><p>实际项目里，审计报告如果只列“存在漏洞”，研发往往难以处理。更有效的方式是把每个问题写成可复现、可定位、可修复、可复测的闭环。</p><table><tr><th>阶段</th><th>重点</th><th>产出</th></tr><tr><td>资产梳理</td><td>确认代码分支、部署环境、入口接口</td><td>接口清单、敏感功能列表</td></tr><tr><td>静态审计</td><td>定位高风险函数、配置、依赖</td><td>疑似风险点与代码位置</td></tr><tr><td>受控验证</td><td>在授权测试环境验证影响</td><td>复现步骤、请求样例、影响范围</td></tr><tr><td>修复建议</td><td>给出最小改动与长期方案</td><td>修复 PR 或整改说明</td></tr><tr><td>复测确认</td><td>确认漏洞不可复现，避免引入回归</td><td>复测记录、回归用例</td></tr></table><p>一个比较实用的问题记录模板如下：</p><pre><code>问题名称：接口权限校验缺失
影响模块：/admin/user/export
代码位置：UserController.java:128
触发条件：普通登录用户可直接请求导出接口
影响说明：可能导致非授权用户导出用户列表
验证环境：测试环境，账号 role=test_user
修复建议：后端增加权限点校验，导出操作纳入审计日志
复测标准：无权限用户访问返回 403，并记录权限拒绝日志</code></pre><h2>注意点</h2><ul><li>不要在生产环境做破坏性验证，尤其是文件写入、批量导出、压力型请求。</li><li>验证权限问题时，应使用授权测试账号，避免使用真实高权限账号做无关操作。</li><li>报告里尽量避免夸大影响，能确认到哪里就写到哪里。</li><li>修复建议要区分短期止血和长期治理，例如先加权限校验，再统一权限框架。</li><li>依赖漏洞不要只看 CVE 列表，要确认组件是否可达、功能是否启用。</li><li>涉及敏感数据的截图、日志、请求包应脱敏后再流转。</li></ul><h2>总结</h2><p>内网 Web 资产的安全问题往往不是单个“高深漏洞”，而是长期迭代中留下的工程缺口。一次有效的代码审计，核心不是堆工具结果，而是围绕入口、权限、数据、文件、配置、日志建立清晰判断链。</p><p>我的经验是，最值得长期投入的是三件事：权限校验集中化、文件与数据访问最小化、关键操作可审计。把这些基础能力做好，很多常见风险会自然下降。审计报告也应尽量服务于修复闭环，让研发能按代码位置和复测标准逐项处理，这比单纯列风险更有价值。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">290</guid><pubDate>Sun, 11 Oct 2026 23:22:31 +0000</pubDate></item><item><title>&#x4E00;&#x6B21; Java Web &#x6587;&#x4EF6;&#x8BFB;&#x53D6;&#x6F0F;&#x6D1E;&#x7684;&#x590D;&#x76D8;&#xFF1A;&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x5230;&#x4FEE;&#x590D;&#x52A0;&#x56FA;</title><link>https://www.hackhat.org/blogs/entry/289-%E4%B8%80%E6%AC%A1-java-web-%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%A4%8D%E7%9B%98%EF%BC%9A%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%88%B0%E4%BF%AE%E5%A4%8D%E5%8A%A0%E5%9B%BA/</link><description><![CDATA[<h2>背景</h2><p>文件读取类漏洞在 Java Web 项目里并不稀奇，但实际排查时经常被低估。很多业务代码本意只是提供“下载日志”“导出附件”“预览模板”等功能，参数看起来也只是一个文件名，一旦路径拼接和权限校验处理不严，就可能变成任意文件读取。</p><p>这篇文章整理一次相对典型的代码审计和复盘过程。内容只讨论本地测试环境中的漏洞成因、验证方式和修复思路，不涉及真实目标攻击，也不提供越权利用指导。重点放在：如何在代码审计中识别风险点，如何构造安全的复现环境，以及如何把问题修干净。</p><h2>场景描述</h2><p>某个内部 Java Web 系统提供附件下载接口，接口大致功能如下：</p><ul><li>用户传入文件名参数。</li><li>服务端从固定目录读取文件。</li><li>以流的形式返回给浏览器下载。</li></ul><p>开发侧认为文件都存放在固定目录下，因此没有额外做权限检查。问题出在“固定目录 + 用户输入”这种组合上，如果直接字符串拼接路径，攻击者可能通过路径穿越读取目录外文件。</p><h2>问题代码示例</h2><p>下面是经过简化的示例代码，便于说明问题。实际项目里类似逻辑可能散落在 Controller、Service 或工具类中。</p><pre><code>String baseDir = "/data/app/uploads/";
String fileName = request.getParameter("file");
File file = new File(baseDir + fileName);

if (!file.exists()) {
    response.setStatus(404);
    return;
}

try (InputStream in = new FileInputStream(file);
     OutputStream out = response.getOutputStream()) {
    byte[] buffer = new byte[8192];
    int len;
    while ((len = in.read(buffer)) != -1) {
        out.write(buffer, 0, len);
    }
}</code></pre><p>这段代码的问题比较直接：</p><ul><li>用户输入直接参与文件路径拼接。</li><li>没有做路径规范化处理。</li><li>没有校验最终文件是否仍位于允许目录下。</li><li>接口层缺少业务权限判断，例如文件是否属于当前用户或当前租户。</li></ul><h2>漏洞成因分析</h2><p>文件读取漏洞的核心并不只是“出现了 ../”，而是服务端没有把“用户可控文件标识”和“真实文件系统路径”隔离开。常见成因有几类。</p><h2>1. 直接拼接路径</h2><p>这是最常见的问题。开发者把用户传入的文件名直接拼到基础目录后面，认为基础目录已经限制了访问范围。但文件系统会解析路径中的相对目录符号，最终可能跳出预期目录。</p><pre><code>File file = new File(baseDir + userInput);</code></pre><p>即使对参数做了简单黑名单过滤，例如替换 ../，也容易被编码、不同分隔符、重复编码或平台差异影响。单纯靠字符串替换不是稳定的安全边界。</p><h2>2. 只判断文件是否存在</h2><p>不少代码只检查 exists() 或 isFile()，但这些判断只能说明路径对应的文件存在，不能说明这个文件是否应该被当前用户访问。</p><pre><code>if (file.exists() && file.isFile()) {
    // 返回文件
}</code></pre><p>安全判断应该包含两个层面：路径边界校验和业务授权校验。前者解决“能不能出目录”，后者解决“是不是你的文件”。</p><h2>3. 使用原始文件名作为下载标识</h2><p>如果数据库中只保存原始文件名，下载接口又允许客户端直接传 fileName，会带来两个问题：</p><ul><li>文件名可预测，容易枚举。</li><li>文件名和路径边界耦合，容易出现路径穿越。</li></ul><p>更稳妥的做法是使用文件 ID 或随机存储名作为引用，由服务端根据数据库记录映射到真实路径。</p><h2>本地复现思路</h2><p>复现建议只在本机或授权测试环境完成。可以用一个临时目录模拟上传目录，不需要读取系统敏感文件。下面以 Linux/macOS 环境为例，准备目录结构：</p><pre><code>mkdir -p /tmp/demo-upload
mkdir -p /tmp/demo-secret
echo "normal attachment" &gt; /tmp/demo-upload/report.txt
echo "should not be downloadable" &gt; /tmp/demo-secret/internal.txt</code></pre><p>假设下载接口的 baseDir 指向 /tmp/demo-upload/，正常访问 report.txt 应该成功，而 /tmp/demo-secret/internal.txt 不应该被读取。测试时只需要验证接口是否可能越过 /tmp/demo-upload 访问上级目录中的测试文件即可。</p><p>如果漏洞存在，服务端日志里通常能看到最终被打开的路径并不在预期目录下。建议在本地调试时打印 canonical path，便于观察路径解析结果。</p><pre><code>System.out.println("request fileName = " + fileName);
System.out.println("absolute path = " + file.getAbsolutePath());
System.out.println("canonical path = " + file.getCanonicalPath());</code></pre><p>这里重点关注 canonical path，因为它会解析相对路径、符号链接等情况，比 getAbsolutePath 更接近真实访问路径。</p><h2>安全修复方案</h2><p>修复文件读取漏洞时，不建议只做参数黑名单。更可靠的方案是“服务端映射 + 路径规范化 + 目录边界校验 + 业务授权”。</p><h2>1. 使用文件 ID 映射真实路径</h2><p>下载接口尽量不要接受任意文件名，而是接受业务文件 ID。服务端根据 ID 查询数据库，确认文件归属和存储路径。</p><pre><code>GET /download?id=12345</code></pre><p>数据库记录中建议至少包含：</p><ul><li>文件 ID。</li><li>上传用户或所属业务对象。</li><li>存储目录或对象存储 key。</li><li>原始文件名，仅用于展示和下载头。</li><li>文件大小、哈希、创建时间等审计字段。</li></ul><p>这样客户端不能自由决定服务端读取哪个路径，风险会低很多。</p><h2>2. 规范化路径并校验目录边界</h2><p>如果确实需要根据相对路径读取文件，必须在读取前做规范化，并确认最终路径仍在允许目录内。</p><pre><code>Path basePath = Paths.get("/data/app/uploads").toRealPath();
String fileName = request.getParameter("file");

Path targetPath = basePath.resolve(fileName).normalize();

if (!targetPath.startsWith(basePath)) {
    response.setStatus(403);
    return;
}

if (!Files.isRegularFile(targetPath)) {
    response.setStatus(404);
    return;
}

try (InputStream in = Files.newInputStream(targetPath);
     OutputStream out = response.getOutputStream()) {
    byte[] buffer = new byte[8192];
    int len;
    while ((len = in.read(buffer)) != -1) {
        out.write(buffer, 0, len);
    }
}</code></pre><p>这里有几个细节：</p><ul><li>basePath 使用 toRealPath() 获取真实路径。</li><li>targetPath 使用 resolve() 和 normalize() 处理相对路径。</li><li>通过 startsWith(basePath) 判断是否仍在允许目录内。</li><li>读取前使用 Files.isRegularFile()，避免目录、设备文件等异常对象。</li></ul><h2>3. 注意符号链接问题</h2><p>符号链接是文件读取修复中容易遗漏的点。如果上传目录内存在指向外部目录的软链接，单纯 normalize() 不一定能覆盖所有情况。更严格的做法是在最终读取前对目标文件调用 toRealPath()，再判断边界。</p><pre><code>Path basePath = Paths.get("/data/app/uploads").toRealPath();
Path targetPath = basePath.resolve(fileName).normalize();

if (!Files.exists(targetPath)) {
    response.setStatus(404);
    return;
}

Path realTargetPath = targetPath.toRealPath();

if (!realTargetPath.startsWith(basePath)) {
    response.setStatus(403);
    return;
}</code></pre><p>如果业务不需要软链接，部署层面也建议禁止在上传目录中创建软链接，或者在文件处理流程中显式拒绝。</p><h2>4. 增加业务权限校验</h2><p>路径边界校验只能证明文件在上传目录里，不能证明当前用户有权下载。多租户系统、工单系统、网盘类系统尤其要注意横向越权。</p><p>推荐的校验流程：</p><ul><li>根据文件 ID 查询文件记录。</li><li>判断文件是否存在且状态正常。</li><li>判断当前用户是否属于该文件的拥有者、协作者或有对应业务对象权限。</li><li>通过权限校验后，再读取对应存储路径。</li></ul><pre><code>Attachment attachment = attachmentDao.findById(fileId);
if (attachment == null) {
    response.setStatus(404);
    return;
}

if (!permissionService.canDownload(currentUser, attachment)) {
    response.setStatus(403);
    return;
}</code></pre><h2>审计时的关注点</h2><p>做代码审计时，可以优先搜索以下关键词和调用点：</p><pre><code>new File(
FileInputStream
Files.newInputStream
Files.readAllBytes
getResourceAsStream
ResourceUtils.getFile
response.getOutputStream
Content-Disposition</code></pre><p>同时关注参数名：</p><pre><code>file
fileName
path
download
template
log
report
attach
url
key</code></pre><p>一些高风险业务接口包括：</p><ul><li>附件下载、图片预览、报表导出。</li><li>日志查看、日志下载。</li><li>模板下载、配置文件预览。</li><li>插件包、升级包、备份文件下载。</li><li>临时文件读取接口。</li></ul><p>审计时不要只看 Controller。很多项目会在工具类里封装 downloadFile、writeFileToResponse、preview 等方法，多个接口复用同一段危险逻辑。</p><h2>测试用例建议</h2><p>修复后建议补充单元测试或集成测试，覆盖正常路径、越界路径、编码路径、软链接等场景。示例用例可以包括：</p><ul><li>正常下载上传目录中的文件。</li><li>访问不存在文件时返回 404。</li><li>访问目录时拒绝。</li><li>相对路径跳出基础目录时返回 403。</li><li>软链接指向外部目录时拒绝。</li><li>无权限用户访问他人文件时返回 403。</li></ul><p>简单的 JUnit 思路如下：</p><pre><code>@Test
void shouldRejectPathTraversal() throws Exception {
    Path base = Files.createTempDirectory("upload");
    Path outside = Files.createTempFile("outside", ".txt");
    Files.writeString(outside, "outside content");

    String input = "../" + outside.getFileName();

    boolean allowed = FilePathChecker.isSafe(base, input);

    assertFalse(allowed);
}</code></pre><p>路径安全判断可以单独抽成工具方法，便于复用和测试。</p><pre><code>public static boolean isSafe(Path baseDir, String userInput) throws IOException {
    Path basePath = baseDir.toRealPath();
    Path targetPath = basePath.resolve(userInput).normalize();

    if (!Files.exists(targetPath)) {
        return false;
    }

    Path realTargetPath = targetPath.toRealPath();
    return realTargetPath.startsWith(basePath) && Files.isRegularFile(realTargetPath);
}</code></pre><h2>日志与监控</h2><p>文件读取类问题修复后，建议增加必要的审计日志。日志不需要记录完整文件内容，但应记录关键上下文，便于后续排查：</p><ul><li>当前用户 ID。</li><li>文件 ID 或业务对象 ID。</li><li>请求来源 IP。</li><li>规范化后的目标路径或存储 key。</li><li>拒绝原因，例如越界、无权限、不存在。</li></ul><p>对明显异常的路径访问可以做告警，例如参数中出现连续的路径跳转符号、异常编码、访问大量不存在文件等。但监控只能作为辅助，不能替代代码层面的边界校验。</p><h2>容易踩的坑</h2><ul><li>只过滤 ../，没有处理 Windows 分隔符或编码差异。</li><li>只判断 absolute path，没有判断 canonical/real path。</li><li>修了下载接口，遗漏了预览接口。</li><li>上传目录允许创建软链接，但读取时没有处理。</li><li>只做路径校验，没有做文件归属权限校验。</li><li>下载头直接使用用户输入文件名，导致响应头异常或兼容性问题。</li></ul><p>下载文件名建议从数据库中的原始文件名读取，并进行安全处理，避免换行符等特殊字符影响响应头。</p><pre><code>String safeName = originalName.replaceAll("[\\r\\n]", "_");
response.setHeader("Content-Disposition", "attachment; filename=\"" + safeName + "\"");</code></pre><h2>总结</h2><p>文件读取漏洞看起来简单，但在真实项目里经常和权限设计、存储设计、历史接口复用混在一起。比较稳妥的治理方式不是临时加几个字符串过滤，而是把文件访问抽象成统一能力：客户端只传文件 ID，服务端负责权限判断、路径映射和边界校验。</p><p>如果只能先做短期修复，至少要做到三点：规范化路径、校验最终真实路径仍在允许目录内、确认当前用户有业务访问权限。后续再逐步把直接传路径的接口改造成文件 ID 访问模式，减少同类问题反复出现。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">289</guid><pubDate>Sun, 11 Oct 2026 23:19:33 +0000</pubDate></item><item><title>&#x4E00;&#x6B21; SSRF &#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x7684;&#x5B8C;&#x6574;&#x65B9;&#x6CD5;&#xFF1A;&#x4ECE;&#x5371;&#x9669; URL &#x5165;&#x53E3;&#x5230;&#x9632;&#x62A4;&#x95ED;&#x73AF;</title><link>https://www.hackhat.org/blogs/entry/288-%E4%B8%80%E6%AC%A1-ssrf-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E7%9A%84%E5%AE%8C%E6%95%B4%E6%96%B9%E6%B3%95%EF%BC%9A%E4%BB%8E%E5%8D%B1%E9%99%A9-url-%E5%85%A5%E5%8F%A3%E5%88%B0%E9%98%B2%E6%8A%A4%E9%97%AD%E7%8E%AF/</link><description><![CDATA[<p>SSRF（Server-Side Request Forgery，服务端请求伪造）在实际项目中并不一定表现为一个明显的“请求外部 URL”接口。图片抓取、Webhook 调试、文件导入、URL 预览、第三方回调检测等功能，都可能让服务器代替用户发起网络请求。审计这类问题时，单纯搜索 <code>requests.get</code> 或 <code>HttpClient</code> 往往不够，更重要的是沿着“用户可控输入—URL 解析—网络连接—响应处理”的完整链路确认风险边界。</p>

<h2>一、问题背景与典型场景</h2>
<p>SSRF 的核心不是“服务器访问了一个 URL”，而是攻击者能够影响服务器访问的目标、协议、端口或请求内容，并借助服务器所在网络环境获得普通客户端无法获得的访问能力。</p>
<ul>
<li>图片或网页预览：后端接收 URL，下载内容后返回给前端。</li>
<li>Webhook 测试：用户输入回调地址，服务端向该地址发送测试请求。</li>
<li>文档解析：服务端根据文档中的外部资源地址加载图片、字体或样式。</li>
<li>云资源集成：后端根据用户提交的地址访问对象存储、内部 API 或代理服务。</li>
<li>健康检查功能：管理员输入地址，系统主动探测服务是否可用。</li>
</ul>
<p>这些功能本身未必存在问题，真正需要关注的是：目标地址是否完全可控，校验发生在 DNS 解析前还是后，是否允许跟随重定向，以及请求是否携带了内部身份凭据。</p>

<h2>二、审计时先画出完整数据流</h2>
<p>建议不要从某一个危险函数开始，而是先建立数据流图。一个典型链路如下：</p>
<pre><code>HTTP 参数 / JSON 字段
        ↓
URL 规范化与解析
        ↓
协议、主机、端口校验
        ↓
DNS 解析
        ↓
建立 TCP / TLS 连接
        ↓
发送请求并处理重定向
        ↓
读取响应、保存文件或转发给用户</code></pre>
<p>审计时可以分别回答以下问题：</p>
<ul>
<li>输入来自哪里，是否经过了鉴权和权限校验？</li>
<li>校验的是原始字符串、解析后的主机名，还是最终连接的 IP 地址？</li>
<li>是否允许 <code>http</code> 以外的协议，例如 <code>file</code>、<code>gopher</code> 或自定义协议？</li>
<li>DNS 解析是否可能返回多个地址，程序是否逐个检查？</li>
<li>请求是否自动跟随 30x 重定向，重定向后的地址是否重新校验？</li>
<li>是否存在代理配置、IPv6、Unix Socket 或其他非预期的网络路径？</li>
<li>响应内容是否会被当作 HTML、XML、图片或压缩包继续解析？</li>
</ul>

<h2>三、最容易被忽略的校验绕过点</h2>
<h2>1. 只做字符串黑名单</h2>
<p>例如仅判断 URL 中是否包含 <code>127.0.0.1</code>、<code>localhost</code> 或某个内部域名。这种做法很脆弱，因为同一个地址可能存在不同表示形式，IPv4、IPv6、十进制整数、短格式地址以及大小写和尾点等写法，都可能导致字符串判断与实际连接目标不一致。</p>
<p>正确思路是先使用标准 URL 解析器拆分协议、主机、端口和路径，再对规范化后的主机进行解析，并对解析结果执行 IP 分类判断。校验对象应该是“最终要建立连接的 IP”，而不是未经解析的原始字符串。</p>

<h2>2. DNS 解析与连接之间存在时间窗口</h2>
<p>有些实现先调用 DNS 解析判断地址是否安全，之后又把原始域名交给 HTTP 客户端建立连接。两次解析的结果可能不同，尤其是在攻击者可控 DNS、短 TTL 或代理环境下，检查结果和实际连接目标可能不一致。</p>
<p>更稳妥的做法是：</p>
<ul>
<li>由应用统一完成解析并取得地址列表。</li>
<li>对所有解析结果进行安全判断，而不是只检查第一个地址。</li>
<li>建立连接时固定使用已校验的地址，同时保留正确的 Host 和 TLS Server Name。</li>
<li>在网络层再增加出口控制，避免应用层校验失效后直接访问敏感网段。</li>
</ul>

<h2>3. 重定向导致校验失效</h2>
<p>初始地址可能是公开站点，但服务器返回重定向后，目标变成了内网地址。如果 HTTP 客户端默认跟随重定向，初始校验就失去了意义。</p>
<p>推荐默认关闭自动重定向。如果业务必须支持重定向，应当在每一次跳转前重新执行协议、主机、端口和解析结果校验，并限制最大跳转次数。</p>

<h2>四、一个防御性 URL 校验示例</h2>
<p>下面示例展示的是审计和防护思路，不负责发起请求。实际项目中还需要结合网络出口策略和 HTTP 客户端的连接控制。</p>
<pre><code>from ipaddress import ip_address
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}


def resolve_all(hostname, port):
    result = socket.getaddrinfo(
        hostname,
        port,
        type=socket.SOCK_STREAM,
    )
    return {item[4][0] for item in result}


def is_public_ip(value):
    ip = ip_address(value)
    return not (
        ip.is_private
        or ip.is_loopback
        or ip.is_link_local
        or ip.is_multicast
        or ip.is_reserved
        or ip.is_unspecified
    )


def validate_url(raw_url):
    parsed = urlparse(raw_url)

    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError("unsupported scheme")
    if not parsed.hostname:
        raise ValueError("missing hostname")

    port = parsed.port or (443 if parsed.scheme == "https" else 80)
    if port not in ALLOWED_PORTS:
        raise ValueError("unsupported port")

    addresses = resolve_all(parsed.hostname, port)
    if not addresses:
        raise ValueError("hostname has no address")

    if not all(is_public_ip(addr) for addr in addresses):
        raise ValueError("destination is not public")

    return {
        "scheme": parsed.scheme,
        "hostname": parsed.hostname,
        "port": port,
        "path": parsed.path or "/",
    }</code></pre>
<p>这个示例有几个值得注意的点：只允许业务需要的协议；端口采用白名单而不是简单排除少数端口；对 DNS 返回的全部地址进行检查；同时拒绝回环、私有、链路本地、组播、保留和未指定地址。</p>
<p>不过，应用层代码仍然可能受到解析与连接之间的竞态影响。因此生产环境不应只依赖这段逻辑，还应通过防火墙、容器网络策略、云安全组或 egress proxy 限制服务端出站访问。</p>

<h2>五、HTTP 客户端配置中的关键防线</h2>
<p>审计代码时，除了查找 URL 参数，还要检查 HTTP 客户端的默认行为。以下配置项经常决定漏洞是否可以被稳定利用：</p>
<table>
<tr><td>配置项</td><td>风险</td><td>建议</td></tr>
<tr><td>自动跟随重定向</td><td>初始地址通过校验后跳转到受限网络</td><td>默认关闭；每次跳转重新校验</td></tr>
<tr><td>连接超时</td><td>内网不可达地址造成线程、协程或连接池耗尽</td><td>设置较短的连接和读取超时</td></tr>
<tr><td>响应体上限</td><td>下载超大文件导致内存或磁盘资源耗尽</td><td>限制响应大小并流式读取</td></tr>
<tr><td>代理继承</td><td>环境变量或全局代理改变实际网络路径</td><td>明确配置代理，不要无意继承</td></tr>
<tr><td>协议处理器</td><td>非 HTTP 协议带来本地文件或其他资源访问能力</td><td>显式限制为业务所需协议</td></tr>
<tr><td>请求头转发</td><td>把用户请求中的 Cookie、Authorization 等凭据转发给目标</td><td>建立明确的请求头白名单</td></tr>
</table>

<h2>六、代码审计中的搜索与回溯方法</h2>
<p>在大型代码库中，可以先进行关键词粗筛，再结合调用链回溯。不同语言的关键词不完全相同，但思路一致。</p>
<ul>
<li>输入来源：<code>url</code>、<code>uri</code>、<code>callback</code>、<code>webhook</code>、<code>image</code>、<code>preview</code>、<code>proxy</code>。</li>
<li>网络请求：<code>requests</code>、<code>urllib</code>、<code>HttpClient</code>、<code>RestTemplate</code>、<code>WebClient</code>、<code>axios</code>。</li>
<li>底层连接：<code>socket</code>、<code>connect</code>、<code>getaddrinfo</code>、<code>net.Dial</code>。</li>
<li>重定向和代理：<code>followRedirects</code>、<code>redirect</code>、<code>proxy</code>、<code>NO_PROXY</code>。</li>
</ul>
<p>找到请求函数后，不要立即下结论。需要继续向上确认输入是否可控，向下确认是否存在 DNS 校验、重定向限制、超时和响应体限制。对封装较深的项目，还应关注统一 HTTP SDK、网关代理和异步任务消费者，因为真正的请求可能不在控制器或路由函数中。</p>

<h2>七、在隔离环境中进行安全验证</h2>
<p>漏洞验证应尽量使用自建的测试服务，不要扫描或请求真实内网、云平台管理接口及第三方系统。可以在本地或专用测试网络中启动两个服务：一个作为公开入口，另一个模拟受限服务，并在日志中记录访问来源和请求内容。</p>
<pre><code># 启动一个仅用于测试的本地 HTTP 服务
python3 -m http.server 18080 --bind 127.0.0.1

# 调用待测应用的 URL 预览接口，具体路径按测试项目调整
curl --max-time 5 \
  -H 'Content-Type: application/json' \
  -d '{"url":" \
  
<p>验证重点不是获取敏感信息，而是确认应用是否真的代替客户端发起了请求。可以通过测试服务日志、应用审计日志、出站防火墙日志和请求链路追踪共同确认。测试结束后应清理临时服务、测试数据和放行规则。</p>

<h2>八、修复方案不能只停留在黑名单</h2>
<p>较完整的防护通常分为四层：</p>
<ul>
<li>业务层：尽量不要让用户直接提交任意 URL。对于图片、Webhook 等功能，使用域名或资源 ID 白名单。</li>
<li>应用层：限制协议、端口、跳转次数、响应大小和超时时间；对解析后的全部 IP 做分类检查。</li>
<li>网络层：服务默认禁止访问管理网段、容器网段、云平台链路本地地址和不必要的内部服务，仅放行明确的外部目的地。</li>
<li>监控层：记录目标域名、解析地址、端口、响应状态、耗时、重定向链和拒绝原因，建立异常访问告警。</li>
</ul>
<p>如果业务确实需要访问外部资源，可以考虑引入专用出站代理。应用只向代理提交经过规范化的目标信息，由代理统一执行域名白名单、DNS 校验、IP 限制、内容大小限制和审计记录。这样比让每个业务模块自行实现一套校验逻辑更容易维护。</p>

<h2>九、容易遗漏的延伸风险</h2>
<ul>
<li>响应内容二次解析：下载的 XML、SVG、HTML 或压缩包可能继续触发外部资源加载或路径穿越问题。</li>
<li>凭据泄露：统一 HTTP 客户端可能自动附加服务账号令牌、内部 Cookie 或代理认证信息。</li>
<li>资源耗尽：目标服务长时间不响应、持续返回大文件或返回大量重定向，都可能形成拒绝服务。</li>
<li>IPv6 双栈差异：只检查 IPv4 而忽略 IPv6，会造成策略不一致。</li>
<li>域名白名单过宽：允许整个组织域名后，子域接管、测试环境和遗留服务可能扩大访问范围。</li>
<li>异步任务绕过鉴权：前端接口做了限制，但后台任务队列消费时没有重新校验目标地址。</li>
</ul>

<h2>十、审计结论应如何记录</h2>
<p>一份可复用的漏洞记录至少应包含以下内容：</p>
<ul>
<li>受影响功能、接口和参数。</li>
<li>从输入到网络请求的完整调用链。</li>
<li>是否允许任意协议、端口、重定向和响应类型。</li>
<li>解析前校验与最终连接目标是否一致。</li>
<li>在隔离环境中的验证结果和日志证据。</li>
<li>业务影响、受影响网络范围以及是否可能泄露服务端凭据。</li>
<li>短期修复、长期架构改进和回归测试用例。</li>
</ul>
<p>回归测试不应只测试一个被禁止的地址，还应覆盖重定向、多个 DNS 记录、IPv6、异常端口、超大响应、超时、非法协议和异步任务等场景。否则修复很容易只针对当前样例，而没有真正收紧请求能力。</p>

<h2>总结</h2>
<p>SSRF 审计的关键不在于记住若干黑名单地址，而在于明确“用户控制的 URL 最终连接到了哪里”。可靠的判断应覆盖 URL 解析、DNS 解析、实际连接、重定向和响应处理全过程，并由应用策略与网络出口策略共同兜底。</p>
<p>从工程角度看，最值得优先落实的措施是：限制业务需求范围、关闭或重新校验重定向、对所有解析地址进行检查、设置超时和响应上限、禁止转发敏感请求头，并在网络层实施默认拒绝的出站策略。这样即使某个业务模块的校验出现遗漏，也不会轻易演变成对内部服务和凭据的横向影响。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-cloud.svg" alt="服务器与云环境安全示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">服务器、云资产与权限边界梳理示意</figcaption></figure>]]></description><guid isPermaLink="false">288</guid><pubDate>Sun, 11 Oct 2026 23:18:18 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21;&#x53EF;&#x7591; WebShell &#x544A;&#x8B66;&#x770B;&#x670D;&#x52A1;&#x5668;&#x4FA7;&#x5165;&#x4FB5;&#x6392;&#x67E5;&#x601D;&#x8DEF;</title><link>https://www.hackhat.org/blogs/entry/287-%E4%BB%8E%E4%B8%80%E6%AC%A1%E5%8F%AF%E7%96%91-webshell-%E5%91%8A%E8%AD%A6%E7%9C%8B%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BE%A7%E5%85%A5%E4%BE%B5%E6%8E%92%E6%9F%A5%E6%80%9D%E8%B7%AF/</link><description><![CDATA[<h2>背景</h2><p>最近整理了一次 Web 服务器可疑文件告警的排查过程，场景并不复杂：业务侧反馈某台 PHP 应用服务器被安全设备提示存在疑似 WebShell 文件，但没有明确说明入口点。类似事件在日常应急里很常见，难点通常不在于“删掉一个可疑文件”，而是要判断它从哪里来、是否被执行过、是否还有持久化或横向痕迹。</p><p>这篇文章按一次相对完整的排查链路来写，重点放在防守视角的取证、日志关联、代码审计和加固建议上，不涉及攻击利用细节。</p><h2>问题场景</h2><p>已知信息比较有限：</p><ul><li>服务器为 Linux，运行 Nginx + PHP-FPM。</li><li>业务目录存在近期新增的可疑 PHP 文件。</li><li>安全设备提示文件具有 WebShell 特征。</li><li>业务方不确定是否存在异常登录或数据泄露。</li></ul><p>这类场景建议先不要急着删除文件。直接删除会破坏时间戳、文件内容和权限等取证信息，也可能漏掉其他关联文件。更稳妥的做法是先隔离访问、保全证据，再做关联分析。</p><h2>初始处置：先控风险，再保全</h2><p>如果业务允许，优先在网关、WAF 或 Nginx 层面临时限制该站点的外部访问，避免可疑文件继续被触发。对于生产环境，通常不建议立即关机，因为关机会丢失部分内存态和进程信息。可以先做以下保全：</p><pre><code>mkdir -p /root/incident-$(date +%F)
cd /root/incident-$(date +%F)

# 记录基本系统信息
date &gt; basic.txt
hostname &gt;&gt; basic.txt
whoami &gt;&gt; basic.txt
uname -a &gt;&gt; basic.txt

# 记录进程、网络连接、登录情况
ps auxww &gt; ps_auxww.txt
ss -antup &gt; ss_antup.txt
last -a &gt; last_login.txt
lastb -a &gt; lastb_login.txt 2&gt;/dev/null
w &gt; w.txt

# 记录计划任务
crontab -l &gt; crontab_current_user.txt 2&gt;/dev/null
ls -lah /etc/cron* &gt; cron_dirs.txt 2&gt;/dev/null</code></pre><p>同时对可疑文件做只读复制，保留哈希：</p><pre><code>cp -a /var/www/html/path/suspect.php ./suspect.php.copy
sha256sum /var/www/html/path/suspect.php ./suspect.php.copy &gt; suspect_hash.txt
stat /var/www/html/path/suspect.php &gt; suspect_stat.txt</code></pre><p>注意不要在原文件上直接使用编辑器打开并保存，避免改变元数据。</p><h2>技术分析一：确认可疑文件特征</h2><p>对 WebShell 的初步识别，一般从代码结构、敏感函数、混淆方式和输入来源几方面入手。防守分析中重点不是复现控制效果，而是确认它是否具备远程执行、文件操作、命令执行、信息回显等能力。</p><p>常见风险点包括：</p><ul><li>动态执行：如 eval、assert、preg_replace 的历史危险用法、create_function 等。</li><li>系统调用：如 system、exec、shell_exec、passthru、popen、proc_open。</li><li>文件操作：如 file_put_contents、fwrite、unlink、copy、move_uploaded_file。</li><li>输入来源：如 $_POST、$_GET、$_COOKIE、php://input。</li><li>混淆痕迹：如 base64_decode、gzinflate、str_rot13、chr 拼接、大量不可读变量名。</li></ul><pre><code># 在业务目录内做敏感函数检索，仅用于排查
cd /var/www/html
grep -RIn --exclude-dir=vendor --exclude-dir=node_modules \
  -E "eval\s*\(|assert\s*\(|system\s*\(|exec\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(|base64_decode\s*\(|gzinflate\s*\(" . &gt; /root/incident-grep.txt</code></pre><p>检索结果不能直接等同于木马。很多框架、依赖包或运维脚本也可能使用类似函数。因此要结合文件路径、修改时间、代码风格、提交记录和访问日志判断。比如一个位于 uploads 目录下的 PHP 文件，风险通常高于框架核心目录里的正常代码。</p><h2>技术分析二：从时间线找入口</h2><p>入侵排查里，时间线非常关键。先看可疑文件的创建、修改时间，再回溯这一时间点前后的访问日志、错误日志和上传目录变化。</p><pre><code># 查看近期变更的 PHP 文件
find /var/www/html -type f -name "*.php" -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %p\n" | sort &gt; recent_php_files.txt

# 查看近期上传目录文件
find /var/www/html -type f -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %s %p\n" | sort &gt; recent_all_files.txt</code></pre><p>如果文件系统支持，可以进一步查看 ctime。ctime 不是创建时间，而是 inode 元数据变化时间，但在排查中很有参考价值：</p><pre><code>stat /var/www/html/path/suspect.php</code></pre><p>随后关联 Nginx access log。假设可疑文件修改时间在 10:20 左右，可以提取前后半小时日志：</p><pre><code>grep "10/Oct/2026:10:" /var/log/nginx/access.log &gt; access_10h.txt</code></pre><p>重点关注以下特征：</p><ul><li>是否有上传接口在可疑时间点被访问。</li><li>是否有管理后台、插件安装、模板编辑等高风险功能被调用。</li><li>是否出现异常 User-Agent、非常规 HTTP 方法或大量 4xx/5xx。</li><li>是否从同一 IP 连续访问上传接口和可疑 PHP 文件。</li><li>是否存在对配置文件、备份文件、接口文档的探测请求。</li></ul><p>如果日志量较大，可以先按状态码、路径、IP 聚合：</p><pre><code># 按 IP 统计访问量
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head

# 按 URL 聚合，适合先看异常路径
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -50

# 查看 4xx/5xx 请求
awk '$9 ~ /^[45]/ {print $0}' /var/log/nginx/access.log | head -200</code></pre><h2>技术分析三：排查上传与文件写入链路</h2><p>WebShell 落地常见原因包括上传校验不足、后台弱口令、组件漏洞、部署目录权限过大、历史测试脚本遗留等。排查时不建议只盯着一个可疑文件，而要把“文件如何写入”作为核心问题。</p><p>可以从以下几个方向检查：</p><ul><li>上传接口是否只校验前端或 Content-Type，而没有服务端白名单。</li><li>上传后的文件是否保存在 Web 可执行目录下。</li><li>Nginx 是否错误地将 uploads、attachment、public 等目录交给 PHP 解析。</li><li>后台是否存在文件管理、模板编辑、插件安装等功能。</li><li>应用日志中是否记录了文件上传、用户登录、后台操作。</li></ul><p>一个比较稳妥的 Nginx 加固方式，是禁止上传目录内 PHP 解析：</p><pre><code>location ~* ^/(uploads|attachment|static)/.*\.php$ {
    return 403;
}</code></pre><p>如果业务允许，更推荐让上传文件使用对象存储或独立静态域名，并确保该域名不具备脚本解析能力。即使上传校验出现疏漏，也能降低直接执行风险。</p><h2>技术分析四：检查 PHP-FPM 与权限边界</h2><p>很多服务器被入侵后影响扩大，并不是单个漏洞本身多严重，而是运行权限过大。例如 PHP-FPM 使用高权限用户运行、业务目录整体可写、配置文件包含数据库明文密码且权限过宽。</p><p>建议检查 PHP-FPM 运行用户：</p><pre><code>ps aux | grep php-fpm | grep -v grep
cat /etc/php-fpm.d/ 2&gt;/dev/null | grep -E "^(user|group)\s*="</code></pre><p>再检查 Web 目录权限：</p><pre><code>find /var/www/html -type d -perm -0002 -ls &gt; world_writable_dirs.txt
find /var/www/html -type f -perm -0002 -ls &gt; world_writable_files.txt
find /var/www/html -type f -name "*.php" -perm -0200 -ls | head -100</code></pre><p>原则上，代码目录不应该被 Web 运行用户随意写入。确实需要写入的目录，如缓存、日志、上传目录，应尽量单独隔离，并禁止脚本执行。</p><h2>技术分析五：判断是否存在后续动作</h2><p>确认 WebShell 后，还需要判断是否被访问过、是否执行过异常命令、是否留下持久化。可以从以下位置排查：</p><ul><li>Nginx access/error log：是否请求过可疑文件。</li><li>PHP-FPM slow log/error log：是否有异常报错或慢请求。</li><li>系统认证日志：是否有异常 SSH 登录、sudo 行为。</li><li>计划任务：是否新增异常任务。</li><li>启动项：systemd、rc.local、profile 等是否被修改。</li><li>临时目录：/tmp、/var/tmp、/dev/shm 是否有异常文件。</li></ul><pre><code># 查找临时目录近期文件
find /tmp /var/tmp /dev/shm -type f -mtime -15 -printf "%TY-%Tm-%Td %TH:%TM:%TS %s %p\n" 2&gt;/dev/null | sort

# 查看系统登录与认证日志，路径按发行版不同可能有差异
grep -Ei "Accepted|Failed|sudo|session" /var/log/auth.log 2&gt;/dev/null | tail -300
grep -Ei "Accepted|Failed|sudo|session" /var/log/secure 2&gt;/dev/null | tail -300

# systemd 服务变更排查
find /etc/systemd/system /usr/lib/systemd/system -type f -mtime -30 -ls 2&gt;/dev/null</code></pre><p>如果发现 Web 运行用户发起异常外连，也需要结合 ss 结果、进程启动时间、父子进程关系进一步确认。对于仍在运行的可疑进程，先记录命令行、环境变量、打开文件，再考虑处置：</p><pre><code># 假设 PID 为 1234
cat /proc/1234/cmdline | tr '\0' ' '
ls -lah /proc/1234/cwd /proc/1234/exe
ls -lah /proc/1234/fd | head
cat /proc/1234/environ | tr '\0' '\n' | head</code></pre><h2>代码审计关注点</h2><p>如果初步判断入口在业务代码，建议围绕可疑时间点访问过的接口做定向审计，而不是全站盲扫。以文件上传为例，审计重点包括：</p><ul><li>扩展名校验是否使用服务端白名单，例如仅允许 jpg、png、pdf 等。</li><li>是否校验真实文件内容，避免只看后缀或 MIME。</li><li>上传后的文件名是否随机化，是否保留用户原始文件名。</li><li>是否允许双后缀、大小写绕过、特殊字符路径。</li><li>上传目录是否与代码目录混放。</li><li>是否存在任意路径写入、目录穿越或解压缩覆盖问题。</li></ul><p>一个更安全的上传处理思路是：服务端白名单扩展名、重新生成文件名、固定存储目录、关闭脚本解析、记录上传审计日志。对于图片类文件，可使用图像库重新编码后再存储，减少夹带非预期内容的风险。</p><h2>处置与恢复建议</h2><p>完成证据保全和影响面判断后，再做处置。常见动作包括：</p><ul><li>删除或隔离确认恶意的文件，并记录哈希和路径。</li><li>修复入口漏洞，例如上传校验、后台弱口令、组件漏洞。</li><li>调整目录权限，禁止上传目录脚本执行。</li><li>轮换数据库、后台、服务器账号等敏感凭据。</li><li>检查同集群、同镜像、同代码仓库是否存在相同问题。</li><li>补齐日志留存，避免后续无法追溯。</li></ul><p>如果无法确认攻击者是否获得系统权限，建议不要只在原机上“清理一下继续跑”。更稳妥的方式是基于可信镜像重建环境，回滚干净代码和数据，再将修复后的配置上线。</p><h2>常见误区</h2><ul><li>只删除 WebShell，不找入口。这样很容易复发。</li><li>只看文件修改时间，不关联访问日志。时间线会断。</li><li>把 grep 到敏感函数的文件都当木马。容易误判业务代码。</li><li>让上传目录继续支持 PHP 解析。修复上传校验也不能替代执行隔离。</li><li>忽略凭据轮换。配置文件、环境变量、日志中都可能暴露敏感信息。</li></ul><h2>总结</h2><p>WebShell 告警的排查重点不是单点文件识别，而是围绕“落地、执行、扩展、持久化”建立证据链。实际工作中，可以按这条线推进：先隔离和保全，再确认文件特征，随后用时间线关联访问日志和业务日志，最后修复入口并加固权限边界。</p><p>长期看，减少这类事件的关键不只是上设备告警，而是把基础防线做好：上传目录不可执行、代码目录最小权限、后台强认证、组件及时更新、日志可追溯。很多入侵并不依赖复杂技巧，往往是几个基础配置同时失守后被放大。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-network.svg" alt="网络流量与边界分析示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">网络请求、日志与边界流量分析示意</figcaption></figure>]]></description><guid isPermaLink="false">287</guid><pubDate>Sun, 11 Oct 2026 23:15:36 +0000</pubDate></item><item><title>&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x5230;&#x4FEE;&#x590D;&#x9A8C;&#x8BC1;&#xFF1A;&#x7CFB;&#x7EDF;&#x68B3;&#x7406; SSRF &#x98CE;&#x9669;&#x7684;&#x8BC6;&#x522B;&#x4E0E;&#x9632;&#x62A4;</title><link>https://www.hackhat.org/blogs/entry/286-%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%88%B0%E4%BF%AE%E5%A4%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E7%B3%BB%E7%BB%9F%E6%A2%B3%E7%90%86-ssrf-%E9%A3%8E%E9%99%A9%E7%9A%84%E8%AF%86%E5%88%AB%E4%B8%8E%E9%98%B2%E6%8A%A4/</link><description><![CDATA[<p>SSRF（Server-Side Request Forgery，服务器端请求伪造）并不是一个只靠“禁止访问内网 IP”就能彻底解决的问题。实际审计中，漏洞往往出现在图片抓取、Webhook 回调、文档转换、远程资源导入、健康检查等功能里。开发者通常只关注了参数是否为合法 URL，却忽略了 URL 解析差异、DNS 解析变化、重定向链以及非 HTTP 协议等边界情况。</p><p>本文以代码审计和修复验证为主线，整理一套适用于 Web 应用的 SSRF 分析方法。示例均以本地测试服务或受控地址为前提，不涉及访问真实内网资产、云平台元数据或第三方系统。</p><h2>一、典型业务场景与风险边界</h2><p>常见的 SSRF 入口包括以下几类：</p><ul><li>根据用户提交的 URL 下载头像、附件或远程图片。</li><li>服务端生成 PDF、截图或预览页面时访问指定地址。</li><li>Webhook、回调地址、消息推送等功能由用户配置目标 URL。</li><li>导入 RSS、OpenAPI 文档或远程配置文件。</li><li>后台健康检查功能允许管理员输入待检测地址。</li></ul><p>这类功能本身并不一定存在漏洞，关键在于服务端是否会按照用户可控的地址发起网络请求，以及请求是否能够到达不应暴露的网络区域。风险通常包括内部管理面板暴露、服务端口探测、云环境敏感接口访问、内网服务未授权调用以及敏感响应回显。</p><blockquote>审计时应区分“服务端可以发起请求”和“攻击者可以利用该请求访问敏感资源”两个层次。前者是行为特征，后者才是需要重点确认的安全影响。</blockquote><h2>二、代码审计时优先关注的危险模式</h2><p>以 Python 为例，下面的代码虽然简短，但将用户输入直接交给网络库，风险边界完全由输入决定：</p><pre><code>import requests

def fetch_remote(url):
    response = requests.get(url, timeout=10)
    return response.content</code></pre><p>审计时不要只搜索 <code>requests.get</code>。还应关注以下调用链：</p><ul><li>HTTP 客户端：<code>requests</code>、<code>urllib</code>、<code>httpx</code>、Java 的 <code>URL</code>、<code>HttpClient</code>、Go 的 <code>http.Get</code> 等。</li><li>间接访问：图片处理器、HTML 转 PDF、截图组件、文档解析器和第三方 SDK。</li><li>协议处理：<code>file</code>、<code>ftp</code>、<code>gopher</code> 等非预期协议是否被底层库支持。</li><li>重定向逻辑：请求库是否默认跟随 30x，重定向后的地址是否重新校验。</li><li>响应处理：是否将响应内容原样返回、写入本地文件，或交给后续解析器。</li></ul><p>建议从路由入口开始，沿着“参数接收—URL 解析—DNS 解析—建立连接—跟随重定向—处理响应”的链路逐段确认，而不是只在某个危险函数附近做字符串匹配。</p><h2>三、URL 校验中最容易出现的误区</h2><h3>1. 只判断字符串前缀</h3><p>类似 <code>url.startswith(' 的校验并不可靠。合法的 URL 语法允许用户名、端口、路径和编码字符参与解析，单纯的前缀判断无法准确表示目标主机。</p><h3>2. 只检查一次 DNS 结果</h3><p>如果应用先解析域名判断是否为公网地址，之后网络库又重新解析一次，就可能出现判断结果与实际连接目标不一致的情况。DNS 记录变化、解析器差异和缓存策略都可能造成这种问题。</p><h3>3. 只禁止几个常见内网地址</h3><p>需要同时考虑 IPv4、IPv6、回环地址、链路本地地址、私有地址、保留地址以及 IPv4 映射的 IPv6 地址。仅过滤 <code>127.0.0.1</code>、<code>10.0.0.0/8</code> 等少数网段，容易留下遗漏。</p><h3>4. 校验原始地址，却信任重定向</h3><p>即使初始地址是允许的公网域名，目标服务器也可能返回一个指向受限地址的重定向。安全策略应在每一次跳转前重新解析并校验目标，并限制最大跳转次数。</p><h2>四、一个更稳妥的校验思路</h2><p>生产环境不建议自行拼装复杂的 URL 黑名单。更可靠的方式是采用“协议白名单、主机白名单、解析后地址校验、连接过程限制”组合策略。</p><p>下面是一个用于说明思路的 Python 示例。它不负责下载内容，只展示如何在请求前对目标进行约束。实际项目中还需要结合网络出口策略和统一 HTTP 客户端实现。</p><pre><code>from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {'https'}
ALLOWED_HOSTS = {'static.example.test'}
BLOCKED_NETS = [
    ip_network('10.0.0.0/8'),
    ip_network('172.16.0.0/12'),
    ip_network('192.168.0.0/16'),
    ip_network('127.0.0.0/8'),
    ip_network('169.254.0.0/16'),
    ip_network('::1/128'),
    ip_network('fc00::/7'),
    ip_network('fe80::/10'),
]

def is_blocked_ip(value):
    ip = ip_address(value)
    return any(ip in network for network in BLOCKED_NETS) or ip.is_private or ip.is_loopback or ip.is_link_local

def validate_target(raw_url):
    parsed = urlparse(raw_url)

    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError('unsupported scheme')
    if parsed.username or parsed.password:
        raise ValueError('userinfo is not allowed')
    if not parsed.hostname or parsed.port not in (None, 443):
        raise ValueError('invalid host or port')
    if parsed.hostname.lower() not in ALLOWED_HOSTS:
        raise ValueError('host is not allowlisted')

    addresses = socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM)
    for item in addresses:
        resolved = item[4][0]
        if is_blocked_ip(resolved):
            raise ValueError('resolved address is not allowed')

    return parsed</code></pre><p>这个示例仍然不是完整的通用组件，原因在于 DNS 校验和实际建连之间可能存在时间窗口。因此更推荐以下方案：</p><ul><li>优先使用固定域名白名单，而不是允许任意公网域名。</li><li>对允许访问的域名配置固定解析结果，或通过受控代理统一转发。</li><li>禁止用户指定端口，必要时仅允许 443 或明确的业务端口。</li><li>禁用自动重定向，或者对每次重定向重新执行完整校验。</li><li>设置连接超时、读取超时、响应大小上限和最大响应头长度。</li><li>将发起请求的服务放置在隔离网络中，出口防火墙默认拒绝访问管理网段。</li></ul><h2>五、修复验证的安全流程</h2><p>验证 SSRF 修复时，应使用自己控制的测试环境，不要直接探测生产内网或第三方地址。可以准备一个简单的本地 HTTP 服务，用于记录请求方法、路径、请求头和来源地址：</p><pre><code>python3 -m http.server 18080 --bind 127.0.0.1</code></pre><p>然后在测试应用中使用以下几类受控输入验证策略是否生效：</p><ul><li>合法的白名单 HTTPS 域名，确认正常业务不受影响。</li><li>HTTP、FTP 或其他非白名单协议，确认被拒绝。</li><li>带用户名密码、非标准端口或异常编码的 URL，确认解析结果符合预期。</li><li>解析到回环、私有、链路本地和 IPv6 保留地址的测试域名，确认被拒绝。</li><li>返回重定向的测试服务，确认跳转目标会重新校验。</li><li>超大响应、慢响应和连接失败场景，确认不会造成资源耗尽。</li></ul><p>验证重点不是“某个 Payload 能否成功访问”，而是确认所有不符合业务策略的目标都会在请求发出前被拦截，并且错误信息不会向外暴露内部解析结果、网络拓扑或服务版本。</p><h2>六、日志与监控同样重要</h2><p>SSRF 防护不能只依赖代码。建议对服务端主动请求建立结构化日志，但避免记录完整的敏感查询参数。日志至少可以包含请求功能、目标域名、解析地址类别、是否命中策略、失败原因和耗时。</p><p>在网络层，可以监控应用服务器是否突然访问管理网段、云平台保留地址或大量非常规端口。对于图片抓取、Webhook 等高频功能，还应设置单用户和单租户的请求速率限制，防止攻击者利用合法功能进行内部端口探测或资源消耗。</p><h2>七、常见修复失败案例</h2><ul><li>在控制器中做了校验，但另一个异步任务入口没有复用同一套策略。</li><li>只校验页面展示的 URL，后台下载器仍然直接使用原始参数。</li><li>过滤了 IPv4 私网段，却遗漏 IPv6 和 IPv4 映射地址。</li><li>限制了初始 URL，却让 HTTP 客户端自动跟随重定向。</li><li>使用黑名单维护所有危险地址，随着网络环境变化逐渐失效。</li><li>应用层校验通过后，容器或主机网络仍可访问不必要的管理服务。</li></ul><p>修复时应将策略封装成统一组件，禁止业务代码自行调用底层网络库。对于确实需要访问任意公网资源的场景，可以考虑部署独立的出站代理，在代理层执行域名解析、地址分类、协议限制和审计记录。</p><h2>总结</h2><p>SSRF 的核心问题不是某个字符串绕过技巧，而是“用户输入是否能够影响服务端的网络边界”。高质量审计需要同时观察代码调用链、URL 解析行为、DNS 与连接过程、重定向处理、响应使用方式以及底层网络隔离。</p><p>实践中，最稳妥的防护组合通常是：默认拒绝、明确的协议和域名白名单、解析后地址校验、禁止或严格控制重定向、限制端口与响应资源，并通过出口防火墙提供最后一道边界。完成修复后，还要使用受控测试环境覆盖 IPv4、IPv6、重定向、超时和异常解析等场景，避免只修复一个入口，却把同样的风险留在其他业务链路中。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">286</guid><pubDate>Sun, 11 Oct 2026 23:11:57 +0000</pubDate></item><item><title>&#x4E00;&#x6B21;&#x8F83;&#x5B8C;&#x6574;&#x7684; SSRF &#x6F0F;&#x6D1E;&#x5206;&#x6790;&#x4E0E;&#x52A0;&#x56FA;&#x601D;&#x8DEF;&#x6574;&#x7406;</title><link>https://www.hackhat.org/blogs/entry/285-%E4%B8%80%E6%AC%A1%E8%BE%83%E5%AE%8C%E6%95%B4%E7%9A%84-ssrf-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B8%8E%E5%8A%A0%E5%9B%BA%E6%80%9D%E8%B7%AF%E6%95%B4%E7%90%86/</link><description><![CDATA[<h2>背景</h2><p>SSRF（Server-Side Request Forgery）这类问题在代码审计和漏洞复现里很常见，但真正处理起来并不只是“禁用内网地址”这么简单。很多业务会天然需要服务端发起请求，例如：远程图片抓取、Webhook 回调、在线文档预览、URL 健康检查、第三方接口转发等。如果只做简单黑名单，通常很容易被各种地址表示法、跳转链、DNS 解析变化绕过去。</p><p>这篇文章整理一个偏工程化的 SSRF 分析方法，重点放在安全验证、漏洞定位和加固落地。内容不会展开攻击性利用，只讨论如何识别风险、复现边界和修复设计。</p><h2>典型场景</h2><p>下面这些接口在审计中需要重点关注：</p><ul><li>根据用户提交的 URL 抓取图片、头像、封面、文件。</li><li>Webhook、Callback、OpenAPI 回调地址配置。</li><li>在线代理、站点截图、URL 预览、网页转 PDF。</li><li>后端请求第三方资源，并将响应内容返回给前端。</li><li>文件导入功能支持 HTTP/HTTPS 远程地址。</li></ul><p>一个常见的风险接口可能长这样：</p><pre><code>POST /api/fetch-image HTTP/1.1
Content-Type: application/json

{
  "url": "
}</code></pre><p>如果服务端直接拿这个 url 发请求，并把结果返回或保存，就需要考虑 SSRF 风险。</p><h2>问题本质</h2><p>SSRF 的核心不是“请求了一个 URL”，而是“用户可控输入影响了服务端的网络访问目标”。服务端一般处在更高权限的网络位置，可能能访问到公网用户无法访问的内部服务、云厂商元数据接口、管理面板、Redis、Elasticsearch、Kubernetes API 等。</p><p>从代码角度看，危险模式通常是：</p><pre><code>// 伪代码，仅用于说明风险模式
url := request.json["url"]
resp := httpClient.Get(url)
return resp.Body</code></pre><p>这段代码的问题不在于使用了 HTTP 客户端，而在于没有对访问目标、协议、解析结果、跳转行为和响应体做任何约束。</p><h2>技术分析</h2><h2>1. URL 解析不一致</h2><p>不同语言、不同库对 URL 的解析行为不完全一致。业务代码校验时使用一个解析器，真正请求时 HTTP 客户端又使用另一个解析器，就可能产生判断差异。审计时要确认校验和请求是否基于同一套解析结果。</p><p>建议至少检查以下字段：</p><ul><li>scheme：只允许 http、https。</li><li>host：必须存在，且不能是空值。</li><li>port：只允许业务必要端口，例如 80、443。</li><li>userinfo：不建议允许，例如  URL 中的特殊字符、编码、大小写、尾点域名、IPv6 表示等情况。修复时不要依赖字符串 contains、startsWith 这类简单判断。</p><h2>2. DNS 解析与地址校验</h2><p>只校验域名字符串是不够的。安全判断应该发生在 DNS 解析之后，对解析得到的 IP 地址进行分类检查。需要拒绝的地址范围一般包括：</p><ul><li>loopback：127.0.0.0/8、::1。</li><li>private：10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。</li><li>link-local：169.254.0.0/16、fe80::/10。</li><li>multicast、reserved、unspecified。</li><li>云元数据服务常见地址，例如 169.254.169.254。</li></ul><p>另外要注意 DNS Rebinding。也就是第一次解析通过校验，实际请求时又解析到内部地址。稳妥做法是：由应用自己解析域名并校验 IP，然后建立连接时绑定到已校验的 IP，同时保留原始 Host/SNI 的业务需求；或者在网络层做 egress 策略，避免应用层校验成为唯一防线。</p><h2>3. 跳转链风险</h2><p>很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只检查初始 URL，不检查跳转后的 Location，仍然存在风险。建议禁用自动跳转，手动处理每一次 Location，并对新的 URL 重复完整校验。</p><pre><code>// Go 示例：禁用自动跳转，避免只校验首个 URL
client := &http.Client{
    Timeout: 5 * time.Second,
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}</code></pre><p>如果业务确实需要跳转，建议限制最大跳转次数，例如不超过 3 次，并要求每次跳转后的 scheme、host、解析 IP 都通过校验。</p><h2>4. 协议与客户端能力</h2><p>SSRF 不一定只发生在 HTTP。部分库或组件支持 file、gopher、ftp、dict、jar 等协议，或者在某些封装层里会自动处理特殊协议。实际审计时要看最终调用的客户端支持什么，不要只看接口名。</p><p>一般建议：</p><ul><li>明确只允许 http 和 https。</li><li>拒绝非标准端口，除非业务确实需要。</li><li>不要把用户输入直接传给支持多协议的通用下载器。</li><li>对响应类型和大小做限制，避免被当成内网探测或大文件下载通道。</li></ul><h2>关键步骤：一次安全复现与确认流程</h2><p>为了避免测试行为影响线上环境，建议在本地或测试环境搭建最小复现。流程如下：</p><h2>1. 定位数据流</h2><p>先确认用户输入从入口到服务端请求的位置。代码审计时重点搜索：</p><pre><code>curl_exec
file_get_contents
fsockopen
requests.get
urllib.request
http.Get
RestTemplate
WebClient
OkHttpClient
HttpClient</code></pre><p>同时关注图片处理、导入导出、回调配置等业务代码，不要只搜“url”。很多变量名可能是 avatar、callback、endpoint、source、remotePath。</p><h2>2. 在测试环境验证外连能力</h2><p>可以准备一个自己控制的测试 HTTP 服务，用于确认服务端是否会访问用户提交的地址。这里只做外连确认，不进行内网探测。</p><pre><code>python3 -m http.server 8080</code></pre><p>提交测试 URL 后观察测试服务日志。如果能看到服务端请求，说明入口具备服务端请求能力，后续需要检查限制是否充分。</p><h2>3. 验证过滤边界</h2><p>在授权测试环境里，可以围绕以下点做边界验证：</p><ul><li>是否限制协议，只允许 http/https。</li><li>是否允许跳转，跳转后的地址是否重新校验。</li><li>是否校验 DNS 解析后的 IP。</li><li>IPv4、IPv6、域名尾点、大小写、编码后 URL 是否处理一致。</li><li>是否限制响应大小、超时时间、Content-Type。</li></ul><p>这里的重点是验证防护逻辑是否完整，而不是扩大影响面。生产环境不建议做内网地址访问测试。</p><h2>加固方案</h2><h2>1. 优先使用白名单</h2><p>如果业务场景固定，例如只允许访问企业自己的对象存储域名、固定第三方 API 域名，白名单是最可靠的方式。不要用“禁止 127.0.0.1、localhost”这类黑名单作为主要防线。</p><pre><code>// 伪代码：基于业务白名单校验 host
allowedHosts := map[string]bool{
    "img.example-cdn.com": true,
    "api.partner.example": true,
}

if !allowedHosts[parsed.Hostname()] {
    return error("host not allowed")
}</code></pre><p>如果必须允许用户输入任意公网 URL，则需要组合使用 URL 规范化、DNS 解析校验、跳转校验、网络出口限制。</p><h2>2. 解析后校验 IP 范围</h2><p>校验逻辑应基于解析后的 IP，而不是原始字符串。下面是一个偏思路化的 Go 示例：</p><pre><code>func isBlockedIP(ip net.IP) bool {
    if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() {
        return true
    }
    // 额外阻断云元数据地址
    if ip.String() == "169.254.169.254" {
        return true
    }
    return false
}</code></pre><p>实际项目里还要补充 IPv6、保留地址段和公司内部网段。不要只判断字符串前缀。</p><h2>3. 控制 HTTP 客户端行为</h2><ul><li>设置较短超时，例如连接超时 2 秒、整体超时 5 秒。</li><li>禁用自动跳转，或对每次跳转重新校验。</li><li>限制最大响应体大小，例如 5MB。</li><li>限制 Content-Type，例如只允许 image/png、image/jpeg。</li><li>禁止携带内部凭据、Cookie、Authorization 头。</li><li>不要把响应体原样返回给用户，避免变成代理通道。</li></ul><pre><code>// Go 示例：限制响应体大小
resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

limited := io.LimitReader(resp.Body, 5*1024*1024)
body, err := io.ReadAll(limited)</code></pre><h2>4. 网络层兜底</h2><p>应用层校验容易因为边界情况遗漏，所以建议配合网络层策略：</p><ul><li>容器或主机层限制出站访问，只允许访问必要公网地址。</li><li>云环境中阻断对元数据服务的非必要访问。</li><li>使用独立的下载服务，放在低权限网络区域。</li><li>对服务端主动外连建立审计日志，包括目标域名、解析 IP、状态码、耗时。</li></ul><p>在 Kubernetes 环境，可以使用 NetworkPolicy 限制 Pod 出站访问。具体策略要结合 CNI 能力和业务依赖设计，不能只写一条“允许全部”。</p><h2>代码审计检查清单</h2><table><tr><th>检查项</th><th>关注点</th><th>建议</th></tr><tr><td>URL 来源</td><td>是否由用户完全可控</td><td>优先改为白名单或后端固定配置</td></tr><tr><td>协议限制</td><td>是否允许 file、ftp 等非 HTTP 协议</td><td>只允许 http/https</td></tr><tr><td>DNS 校验</td><td>是否只判断域名字符串</td><td>解析后校验 IP 分类</td></tr><tr><td>跳转处理</td><td>是否默认跟随 30x</td><td>禁用自动跳转或逐跳校验</td></tr><tr><td>响应限制</td><td>是否无限读取响应体</td><td>限制大小、类型和超时</td></tr><tr><td>凭据泄露</td><td>是否转发 Cookie、Token</td><td>外连请求使用干净 Header</td></tr><tr><td>网络出口</td><td>服务是否能访问内网敏感段</td><td>通过防火墙或 NetworkPolicy 兜底</td></tr></table><h2>常见误区</h2><ul><li>只过滤 localhost。实际还有 127.0.0.1、::1、内网段、link-local、各种域名解析情况。</li><li>只在前端校验 URL。服务端必须独立校验。</li><li>只校验第一次请求。跳转后的地址同样需要校验。</li><li>认为 HTTPS 就安全。HTTPS 只能保证传输安全，不代表目标地址可信。</li><li>把 SSRF 当成单点漏洞修补。更合理的是把外连能力当成高风险能力统一治理。</li></ul><h2>事件复盘建议</h2><p>如果已经发现 SSRF 风险，复盘时建议关注三条线：</p><ul><li>影响面：哪些接口存在服务端外连能力，是否可被未授权或低权限用户触发。</li><li>可达性：相关服务能访问哪些内部网段、云元数据、管理接口。</li><li>数据暴露：接口是否会返回响应体、响应头、错误信息或时间差。</li></ul><p>日志侧建议检索异常目标地址、短时间大量不同 URL、访问失败率异常、非业务域名请求等。对于历史日志不足的系统，至少在修复后补充外连审计，方便后续追踪。</p><h2>总结</h2><p>SSRF 的治理重点不是写一个复杂正则，而是建立完整的访问目标控制链路：输入规范化、协议限制、白名单优先、DNS 解析后校验、跳转逐次校验、HTTP 客户端收敛、网络出口兜底。单靠某一层很难覆盖所有边界。</p><p>从长期维护角度看，建议把“服务端主动访问外部 URL”抽象成统一组件，业务侧不要各自实现下载逻辑。统一组件里沉淀校验、日志、超时、响应大小限制和安全策略，后续审计和加固都会轻松很多。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-cloud.svg" alt="服务器与云环境安全示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">服务器、云资产与权限边界梳理示意</figcaption></figure>]]></description><guid isPermaLink="false">285</guid><pubDate>Sun, 11 Oct 2026 23:10:33 +0000</pubDate></item><item><title>&#x4ECE;&#x8BF7;&#x6C42;&#x8FB9;&#x754C;&#x5230;&#x8BC1;&#x636E;&#x95ED;&#x73AF;&#xFF1A;SSRF &#x6F0F;&#x6D1E;&#x7684;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x3001;&#x9A8C;&#x8BC1;&#x4E0E;&#x4FEE;&#x590D;&#x5B9E;&#x8DF5;</title><link>https://www.hackhat.org/blogs/entry/284-%E4%BB%8E%E8%AF%B7%E6%B1%82%E8%BE%B9%E7%95%8C%E5%88%B0%E8%AF%81%E6%8D%AE%E9%97%AD%E7%8E%AF%EF%BC%9Assrf-%E6%BC%8F%E6%B4%9E%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E3%80%81%E9%AA%8C%E8%AF%81%E4%B8%8E%E4%BF%AE%E5%A4%8D%E5%AE%9E%E8%B7%B5/</link><description><![CDATA[<p>SSRF（Server-Side Request Forgery，服务端请求伪造）并不是“服务端发起了一次 HTTP 请求”这么简单。真正需要关注的是：用户可控数据是否进入了服务端的网络请求链路，服务端是否能够访问不应暴露的网络区域，以及应用、代理和云环境之间是否存在边界失效。</p><p>这类问题在代码审计中比较常见，尤其容易出现在图片抓取、Webhook 调试、文档转换、远程资源导入、URL 预览和第三方接口探测等功能中。本文以防守视角整理一套可复用的方法，重点放在审计思路、低风险验证、修复设计和复测证据上。</p><h2>一、问题场景与风险边界</h2><p>典型业务流程是：用户提交一个 URL，服务端下载内容后进行解析、转码或展示。风险通常来自以下几类位置：</p><ul><li>直接调用 HTTP 客户端，例如 requests、urllib、OkHttp、axios 或内部封装的 fetch 方法。</li><li>用户输入先经过重定向、短链接或 URL 解析，再由后台任务异步访问。</li><li>文件解析器、Markdown 渲染器、网页截图服务间接触发外部请求。</li><li>代理服务允许访问内部域名、管理接口或容器网络。</li><li>应用层做了域名校验，但 DNS 解析、重定向和底层连接过程没有保持一致。</li></ul><p>SSRF 的影响不应只用“能否访问本机”来判断。审计时至少要区分三种边界：</p><ul><li>应用自身边界：是否能访问仅绑定在回环地址的管理接口或调试接口。</li><li>网络边界：是否能访问内网网段、容器网段、办公网或测试环境。</li><li>身份边界：请求是否会自动携带服务端的 Cookie、内部鉴权头或云环境凭证。</li></ul><blockquote><p>验证过程中只使用自建测试服务和明确授权的测试网段，不访问真实云元数据、生产管理接口或第三方内部资源。审计的目标是证明边界存在，而不是扩大影响范围。</p></blockquote><h2>二、代码审计：从输入点追踪到请求出口</h2><p>审计 SSRF 时，不要只搜索“url”变量。更有效的方法是从外部输入开始，沿着数据流追踪到网络请求出口。</p><p>第一步是定位输入点，包括 HTTP 参数、JSON 字段、导入文件中的 URL、消息队列字段以及后台任务参数。第二步是搜索请求出口，例如 requests.get、httpx.Client、urllib.request.urlopen、Java 的 URLConnection、Go 的 http.Client.Do 等。第三步是检查两者之间是否存在完整且一致的校验。</p><p>下面是一个典型的高风险模式。问题不在于使用了某个具体库，而在于用户输入未经边界检查就进入了服务端请求函数：</p><pre><code>def fetch_preview(user_url):
    response = requests.get(user_url, timeout=5, allow_redirects=True)
    return response.text[:20000]</code></pre><p>审计时建议继续向下确认几个细节：</p><ul><li>是否允许 http、https 之外的协议。</li><li>是否允许显式端口，是否限制了端口范围。</li><li>是否校验了域名解析结果，而不是只校验字符串。</li><li>是否跟随重定向，重定向后的目标是否重新校验。</li><li>是否存在代理环境变量、服务网格或网关的二次转发。</li><li>是否会把响应正文、响应头或错误信息返回给用户。</li><li>请求是否自动附带 Cookie、Authorization 或内部服务头。</li></ul><h2>三、低风险验证方法</h2><p>验证时不需要直接探测真实内网。可以在本地或隔离测试环境启动一个简单的 HTTP 服务，分别模拟正常外部站点、禁止访问的测试地址以及重定向场景。</p><p>例如，先启动一个仅用于测试的本地服务：</p><pre><code>python3 -m http.server 18080 --bind 127.0.0.1</code></pre><p>然后在应用的测试环境中提交一个指向该服务的 URL，观察服务端访问日志。验证重点包括：</p><ul><li>请求是否确实由服务端发出，而不是浏览器直接访问。</li><li>应用是否允许访问回环地址。</li><li>应用是否限制了非标准端口。</li><li>应用是否将响应内容或状态信息返回给调用方。</li><li>应用日志中是否记录了原始 URL、最终 URL、解析地址和拒绝原因。</li></ul><p>对于重定向测试，应在自建服务中配置一个跳转到另一个测试地址的响应，确认应用是否对每一次跳转重新执行校验。不要把测试目标替换成真实的内网服务，也不要使用云厂商元数据地址。</p><h2>四、URL 校验中最容易遗漏的细节</h2><h3>1. 协议白名单</h3><p>如果业务只需要抓取网页，通常应只允许 http 和 https。file、ftp、gopher 等协议会扩大攻击面，除非业务有明确需求，否则应在解析阶段拒绝。</p><h3>2. 用户信息与端口</h3><p>URL 中可能包含用户名、密码和显式端口。即便客户端库能够正常解析，也不代表业务应该接受这些字段。建议拒绝 URL 中的 username 和 password，并对端口设置明确白名单。</p><h3>3. IP 字面量与地址类型</h3><p>不能只用字符串判断目标是否为内网地址。IPv4、IPv6、IPv4-mapped IPv6、回环地址、链路本地地址、组播地址和未指定地址都应纳入检查。还要注意十进制、十六进制或其他非标准表示方式可能被不同组件以不同方式解释。</p><h3>4. DNS 解析与重绑定</h3><p>“先解析、后请求”的简单实现仍然可能存在时间窗口：校验阶段解析到公网地址，真正建立连接时又解析到了内部地址。更稳妥的方式是通过统一的出站代理完成解析和连接，并在代理侧执行网络策略；如果必须由应用直连，则需要尽量复用已校验的解析结果，同时在网络层限制出站范围。</p><h3>5. 重定向</h3><p>首次 URL 校验通过并不代表整个请求链路安全。每次重定向都必须重新解析、重新检查协议、域名、IP 和端口，并设置最大跳转次数。更简单的做法是默认关闭自动重定向，只有在业务确实需要时才受控开启。</p><h2>五、一个防守型校验示例</h2><p>下面的示例用于说明校验顺序，不应被当作完整的网络安全边界。生产环境还需要配合出站防火墙、代理策略、连接超时和响应大小限制。</p><pre><code>from ipaddress import ip_address
from socket import getaddrinfo, AF_UNSPEC
from urllib.parse import urlsplit

ALLOWED_SCHEMES = {'http', 'https'}
ALLOWED_PORTS = {80, 443}

def resolve_addresses(hostname):
    results = set()
    for item in getaddrinfo(hostname, None, family=AF_UNSPEC, type=0):
        results.add(item[4][0])
    return results

def validate_target(raw_url):
    parsed = urlsplit(raw_url)

    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        raise ValueError('scheme is not allowed')
    if not parsed.hostname:
        raise ValueError('hostname is required')
    if parsed.username or parsed.password:
        raise ValueError('userinfo is not allowed')

    try:
        port = parsed.port or (443 if parsed.scheme.lower() == 'https' else 80)
    except ValueError:
        raise ValueError('invalid port')

    if port not in ALLOWED_PORTS:
        raise ValueError('port is not allowed')

    addresses = resolve_addresses(parsed.hostname)
    if not addresses:
        raise ValueError('hostname cannot be resolved')

    for item in addresses:
        ip = ip_address(item)
        if (ip.is_private or ip.is_loopback or ip.is_link_local or
                ip.is_multicast or ip.is_unspecified or ip.is_reserved):
            raise ValueError('target address is not allowed')

    return parsed</code></pre><p>这个示例有几个需要特别说明的地方：</p><ul><li>校验对象是解析后的结构，而不是简单字符串匹配。</li><li>同时检查所有解析结果，不能只检查第一个地址。</li><li>解析通过后，不能再无控制地让底层库重新解析原始域名。</li><li>如果存在代理，必须确认代理是否会重新解析域名，以及代理本身的出站策略。</li><li>IPv6 环境下应明确测试地址族，不能假设所有请求都走 IPv4。</li></ul><p>在实际项目中，更推荐将“目标解析、策略判断和建立连接”统一下沉到一个受控的出站组件中。应用只提交经过业务约束的目标，网络组件负责最终的域名解析、地址过滤、重定向处理和出站审计。</p><h2>六、请求层的附加防护</h2><p>仅做地址校验通常还不够，建议同时设置以下限制：</p><ul><li>连接超时、读取超时和总请求时长，避免请求占满工作线程。</li><li>最大响应体积，避免通过远程资源消耗内存和磁盘。</li><li>最大重定向次数，并对每次跳转重新执行策略检查。</li><li>禁止自动发送系统环境中的代理凭据、Cookie 和 Authorization 头。</li><li>限制响应内容类型，只允许业务真正需要的 MIME 类型。</li><li>对后台任务设置并发上限和队列长度，防止被用作内部网络扫描器。</li><li>出站防火墙默认拒绝，仅放行明确需要的目标网段和端口。</li></ul><p>如果业务是图片、文档或网页抓取，还应考虑解析器自身的风险。例如，下载阶段限制了 URL，并不代表后续的 SVG、XML、CSS 或 HTML 解析阶段不会再次发起外部请求。应分别检查下载器、解压器、渲染器和转换器的网络访问能力。</p><h2>七、日志与复测证据</h2><p>SSRF 修复不能只看“接口返回 400”。需要建立能够复核的证据链。建议至少记录以下字段：</p><ul><li>请求标识、业务用户和任务来源。</li><li>原始 URL 的脱敏形式。</li><li>解析后的协议、主机、端口和地址族。</li><li>最终连接地址或代理判定结果。</li><li>拒绝原因，例如协议不允许、地址属于保留网段、端口不在白名单。</li><li>重定向次数、响应状态和响应体大小。</li></ul><p>日志中不要直接记录可能包含凭据的完整 URL，也不要把远程响应原样写入日志。复测时可以建立以下用例矩阵：</p><ul><li>正常公网域名：应按业务规则成功。</li><li>回环地址和本地测试服务：应拒绝。</li><li>私有网段、链路本地地址和 IPv6 回环地址：应拒绝。</li><li>带用户名密码的 URL：应拒绝。</li><li>非 http/https 协议：应拒绝。</li><li>非白名单端口：应拒绝。</li><li>跳转到禁止地址：应在跳转阶段拒绝。</li><li>解析出多个地址，其中一个属于禁止范围：应整体拒绝。</li></ul><h2>八、常见错误做法</h2><ul><li>只检查 URL 是否以某个域名字符串开头，忽略解析边界。</li><li>只在首次请求前校验，允许后续重定向自由跳转。</li><li>使用黑名单拦截少量内网地址，却没有建立协议、端口和出站网络白名单。</li><li>校验时使用一个 DNS 解析器，请求时又由另一个代理重新解析。</li><li>认为“响应内容不返回给用户”就没有风险，忽略了内网探测、资源消耗和侧信道。</li><li>只修复一个接口，没有搜索同一请求封装在其他业务中的复用位置。</li></ul><h2>总结</h2><p>SSRF 的核心不是某个特殊 URL 写法，而是用户输入跨越了应用原本的网络信任边界。有效的治理应当同时覆盖代码审计、协议和地址校验、重定向处理、DNS 一致性、出站网络控制、凭据隔离以及日志复测。</p><p>在审计实践中，可以把问题归纳为一条稳定的检查链：输入点是否可控，是否进入请求出口，请求是否经过统一策略，解析与连接是否一致，重定向是否重新校验，网络层是否有最终阻断，日志是否能够证明策略确实生效。按照这条链路推进，通常比单纯收集各种绕过写法更容易定位根因，也更有利于形成可长期维护的修复方案。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">284</guid><pubDate>Sun, 11 Oct 2026 23:08:56 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21;&#x767B;&#x5F55;&#x63A5;&#x53E3;&#x5F02;&#x5E38;&#x770B; Web &#x98CE;&#x9669;&#x6392;&#x67E5;&#x4E0E;&#x52A0;&#x56FA;&#x601D;&#x8DEF;</title><link>https://www.hackhat.org/blogs/entry/283-%E4%BB%8E%E4%B8%80%E6%AC%A1%E7%99%BB%E5%BD%95%E6%8E%A5%E5%8F%A3%E5%BC%82%E5%B8%B8%E7%9C%8B-web-%E9%A3%8E%E9%99%A9%E6%8E%92%E6%9F%A5%E4%B8%8E%E5%8A%A0%E5%9B%BA%E6%80%9D%E8%B7%AF/</link><description><![CDATA[<h2>背景</h2><p>最近在做一个内部业务系统的安全复盘时，遇到一个比较典型的场景：登录接口没有出现明显漏洞利用痕迹，但日志里能看到大量异常请求，包括高频失败登录、参数结构异常、不同 User-Agent 的批量访问，以及少量疑似探测请求。</p><p>这类问题在实际环境中很常见。它不一定意味着系统已经被入侵，但通常说明接口暴露面、认证策略、日志可观测性和风控策略存在短板。本文不讨论任何攻击利用方法，重点放在如何从防守视角复盘、分析和加固。</p><h2>问题场景</h2><p>业务系统采用常见的前后端分离架构，登录接口大致如下：</p><pre><code>POST /api/auth/login HTTP/1.1
Content-Type: application/json

{
  "username": "test",
  "password": "******",
  "captcha": "1234"
}</code></pre><p>排查时发现几个现象：</p><ul><li>短时间内出现大量登录失败，来源 IP 分散。</li><li>部分请求缺失 captcha 字段，或字段类型不符合预期。</li><li>接口返回信息区分了“用户不存在”和“密码错误”。</li><li>同一账号在不同 IP 下连续失败，但应用层没有统一限速。</li><li>日志记录了请求结果，但没有记录关键风险字段，后续关联分析困难。</li></ul><p>这些问题单独看都不算复杂，但组合在一起会明显提高账号体系被持续探测的风险。</p><h2>技术分析</h2><h2>1. 返回信息过于明确</h2><p>登录接口如果返回“用户不存在”“密码错误”“验证码错误”等过于细粒度的信息，容易暴露账号枚举面。防守上更推荐对外统一返回认证失败，只在服务端日志中记录具体原因。</p><pre><code>// 不推荐
{
  "code": 1001,
  "message": "用户不存在"
}

// 推荐
{
  "code": 401,
  "message": "用户名或密码错误"
}</code></pre><p>需要注意的是，统一返回并不代表丢失排查能力。正确做法是对外模糊，对内详细。</p><pre><code>login_fail reason=user_not_found username_hash=8f14e45f ip=10.1.2.3 ua_hash=...</code></pre><h2>2. 缺少分层限速</h2><p>很多系统只做了简单的 IP 限速，但在实际环境中，单一 IP 限速效果有限。更合理的策略是分层限速，例如：</p><ul><li>按 IP 限速：限制单个来源的高频请求。</li><li>按账号限速：限制同一账号被反复尝试。</li><li>按设备指纹或会话标识限速：辅助识别异常客户端。</li><li>按网段、ASN、代理特征做风险评分，而不是简单封禁。</li></ul><p>Nginx 层可以先做基础频控，应用层再做更精细的策略。</p><pre><code>http {
    limit_req_zone $binary_remote_addr zone=login_ip:10m rate=5r/m;

    server {
        location /api/auth/login {
            limit_req zone=login_ip burst=10 nodelay;
            proxy_pass 
        }
    }
}</code></pre><p>这类配置只能作为第一层防护，不能替代应用层的账号维度风控。</p><h2>3. 参数校验不完整</h2><p>登录接口是高频风险入口，参数校验应尽量前置。常见问题包括：</p><ul><li>username 未限制长度，导致日志污染或异常消耗。</li><li>password 接受非字符串类型，触发框架边界行为。</li><li>captcha 可为空或字段缺失时仍进入密码校验逻辑。</li><li>JSON 解析异常被统一吞掉，导致监控无法识别异常请求。</li></ul><p>建议在进入认证逻辑前完成结构校验，并对异常类型进行分类记录。</p><pre><code>if not isinstance(username, str) or len(username) &gt; 128:
    return auth_failed()

if not isinstance(password, str) or len(password) &gt; 256:
    return auth_failed()

if captcha_required and not valid_captcha(captcha):
    return auth_failed()</code></pre><p>这里的重点不是代码本身，而是避免让异常输入进入核心认证流程。</p><h2>4. 日志字段不足</h2><p>不少系统在出现风险事件后才发现日志不够用。登录接口至少建议记录以下字段：</p><table><tr><td>字段</td><td>用途</td></tr><tr><td>request_id</td><td>串联网关、应用、审计日志</td></tr><tr><td>username_hash</td><td>保护隐私的同时支持账号维度分析</td></tr><tr><td>client_ip</td><td>来源分析，注意结合可信代理头</td></tr><tr><td>user_agent_hash</td><td>聚合异常客户端特征</td></tr><tr><td>fail_reason</td><td>内部排查原因，不对外返回</td></tr><tr><td>risk_score</td><td>风控策略命中情况</td></tr><tr><td>latency_ms</td><td>识别异常耗时和潜在资源消耗</td></tr></table><p>涉及密码、验证码、完整 Token、Cookie 的内容不要写入日志。这个问题在审计中非常常见，后续日志平台权限一旦扩大，敏感信息泄露面会被放大。</p><h2>关键排查步骤</h2><h2>1. 先确认时间线</h2><p>不要一上来就封 IP 或改策略，先确认异常开始时间、峰值时间、持续时间，以及是否和业务发布、营销活动、爬虫流量有关。</p><pre><code># 示例：按分钟统计登录失败数量
cat app.log | grep 'login_fail' | awk '{print $1" "$2}' | cut -c1-16 | sort | uniq -c</code></pre><p>如果日志是 JSON 格式，可以用 jq 做更稳定的分析。</p><pre><code>cat app.log | jq -r 'select(.event=="login_fail") | .time[0:16]' | sort | uniq -c</code></pre><h2>2. 识别异常聚合维度</h2><p>重点不要只看 IP。建议同时聚合以下维度：</p><ul><li>失败次数最多的账号。</li><li>单账号来源 IP 数量。</li><li>单 IP 尝试账号数量。</li><li>User-Agent 分布。</li><li>请求体异常比例。</li><li>验证码失败和密码失败的比例。</li></ul><pre><code># 按 username_hash 聚合失败次数
cat app.log | jq -r 'select(.event=="login_fail") | .username_hash' | sort | uniq -c | sort -nr | head</code></pre><h2>3. 检查认证链路顺序</h2><p>认证链路建议遵循“低成本校验在前，高成本操作在后”的原则。例如：</p><ul><li>请求方法、Content-Type 校验。</li><li>JSON 结构和字段类型校验。</li><li>限速和风险策略判断。</li><li>验证码或二次验证校验。</li><li>账号状态检查。</li><li>密码哈希校验。</li></ul><p>密码哈希计算通常是相对高成本操作，如果大量异常请求都能走到这一步，可能造成不必要的资源压力。</p><h2>4. 检查代理 IP 获取逻辑</h2><p>如果系统部署在反向代理、负载均衡或 CDN 后面，应用获取客户端 IP 时要谨慎。不能直接信任任意请求头中的 X-Forwarded-For。</p><pre><code># 思路示例：只信任来自可信代理的转发头
trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"]

if remote_addr in trusted_proxies:
    client_ip = parse_x_forwarded_for(headers)
else:
    client_ip = remote_addr</code></pre><p>如果这一步做错，限速和审计都会失真。</p><h2>加固建议</h2><h2>1. 对外响应统一</h2><p>登录失败统一返回认证失败，不暴露账号是否存在、是否冻结、验证码是否单独错误等细节。内部日志保留具体原因，方便排查。</p><h2>2. 增加账号维度保护</h2><p>对连续失败的账号进行冷却，例如短时间内多次失败后要求二次验证，或者延迟再次尝试。注意不要简单锁死账号，否则可能被滥用为拒绝服务手段。</p><h2>3. 引入风险评分而非单点拦截</h2><p>可以把 IP 信誉、失败次数、账号敏感度、设备变化、地理位置变化、请求异常程度综合为风险分。风险高时提高验证强度，而不是只依赖固定阈值。</p><h2>4. 敏感日志脱敏</h2><p>用户名建议哈希化后记录，手机号、邮箱保留部分掩码即可。Token、密码、验证码不应进入日志。</p><pre><code>username_hash = sha256(normalize(username) + log_salt)</code></pre><p>这里的 log_salt 建议单独管理，避免被用于反查明文。</p><h2>5. 设置告警规则</h2><p>告警不要只看总量，也要看结构性异常。例如：</p><ul><li>同一账号 10 分钟内失败来源超过 N 个。</li><li>同一 IP 10 分钟内尝试账号超过 N 个。</li><li>登录失败率突然高于历史基线。</li><li>参数校验失败比例异常升高。</li><li>验证码失败量与密码失败量比例异常。</li></ul><p>这类告警比单纯的 QPS 告警更有价值。</p><h2>注意点</h2><ul><li>不要在生产环境直接打印完整请求体，尤其是登录、支付、个人资料接口。</li><li>限速策略上线前要灰度，避免误伤真实用户。</li><li>账号锁定策略要谨慎，避免被利用来批量锁定他人账号。</li><li>如果接入 CDN 或网关，应用层仍应保留基础校验和审计能力。</li><li>排查异常流量时要区分安全风险、业务活动和监控探测，避免误判。</li></ul><h2>总结</h2><p>登录接口安全并不只是“密码校验是否正确”这么简单，它涉及认证反馈、限速、参数校验、日志审计、风险识别和告警联动。很多线上风险不是单个高危漏洞导致的，而是多个小问题叠加后形成了可被持续消耗和探测的入口。</p><p>从长期维护角度看，建议把登录接口当作一个独立的安全资产来管理：有清晰的日志字段，有分层限速，有统一的错误响应，有可回溯的审计链路，也有可灰度调整的风控策略。这样即使遇到异常流量，也能更快判断影响范围，并采取相对稳妥的处置措施。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-network.svg" alt="网络流量与边界分析示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">网络请求、日志与边界流量分析示意</figcaption></figure>]]></description><guid isPermaLink="false">283</guid><pubDate>Sun, 11 Oct 2026 22:57:27 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21; SSRF &#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x51FA;&#x53D1;&#xFF1A;&#x8BC6;&#x522B;&#x8BF7;&#x6C42;&#x8FB9;&#x754C;&#x3001;&#x9A8C;&#x8BC1;&#x98CE;&#x9669;&#x4E0E;&#x5EFA;&#x7ACB;&#x7EB5;&#x6DF1;&#x9632;&#x62A4;</title><link>https://www.hackhat.org/blogs/entry/282-%E4%BB%8E%E4%B8%80%E6%AC%A1-ssrf-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%87%BA%E5%8F%91%EF%BC%9A%E8%AF%86%E5%88%AB%E8%AF%B7%E6%B1%82%E8%BE%B9%E7%95%8C%E3%80%81%E9%AA%8C%E8%AF%81%E9%A3%8E%E9%99%A9%E4%B8%8E%E5%BB%BA%E7%AB%8B%E7%BA%B5%E6%B7%B1%E9%98%B2%E6%8A%A4/</link><description><![CDATA[<p>SSRF（Server-Side Request Forgery，服务端请求伪造）并不只是“让服务器代替用户发起一次请求”。在真实系统中，它往往与网络分区、云环境元数据服务、内部管理接口以及权限模型相互叠加，最终形成从外部输入到内部资源的访问链路。对这类问题进行审计时，单纯搜索 <code>requests.get()</code> 或 <code>HttpClient</code> 并不够，更重要的是理解数据流、请求边界和部署环境。</p><h2>一、问题背景与常见场景</h2><p>SSRF 通常出现在以下功能中：</p><ul><li>根据用户提交的 URL 抓取远程图片、网页或 RSS。</li><li>Webhook、回调地址、第三方通知地址配置。</li><li>在线预览、文档转换、PDF 生成和远程资源解析。</li><li>服务端健康检查、连通性测试和网络探测功能。</li><li>代理下载、跳转跟随和文件导入功能。</li></ul><p>这些功能本身并不一定有问题。风险主要取决于三个因素：用户是否能够影响目标地址，服务端是否会访问不应暴露的网络区域，以及返回内容是否会被直接回显或进一步处理。</p><blockquote><p>审计时应把 SSRF 看作一条完整的数据流，而不是某一行危险函数。输入校验、DNS 解析、连接建立、重定向、响应读取和后续解析，每一个环节都可能改变最终风险。</p></blockquote><h2>二、先画出请求链路，再判断风险</h2><p>一个典型的远程资源获取流程可以抽象为：</p><pre><code>用户输入 URL
    ↓
参数校验与规范化
    ↓
DNS 解析
    ↓
建立 TCP/TLS 连接
    ↓
处理重定向
    ↓
读取响应
    ↓
解析或存储内容</code></pre><p>代码审计时建议沿着这条链路逐项确认：</p><ul><li>输入是否允许完整 URL，还是只允许固定域名下的路径。</li><li>校验发生在 DNS 解析之前还是之后。</li><li>校验通过后是否重新解析 DNS。</li><li>HTTP 客户端是否默认跟随重定向。</li><li>是否允许非 HTTP、HTTPS 协议。</li><li>是否限制响应大小、连接超时和读取超时。</li><li>响应是否会被当作 XML、图片、压缩包或模板继续处理。</li></ul><p>如果只在入口处做字符串判断，例如判断 URL 是否包含某个域名，通常不足以证明安全。URL 解析存在用户名、端口、编码、IPv6、重定向和 DNS 变化等多个边界条件，字符串匹配很容易与实际连接目标不一致。</p><h2>三、一个常见但不可靠的实现</h2><p>下面的示例仅用于说明审计思路。它试图通过字符串判断阻止访问内部地址，但实际防护能力很弱：</p><pre><code>public byte[] fetch(String input) throws IOException {
    if (input.contains("127.0.0.1") || input.contains("localhost")) {
        throw new IllegalArgumentException("blocked");
    }

    URL url = new URL(input);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    return conn.getInputStream().readAllBytes();
}</code></pre><p>这段代码至少存在以下问题：</p><ul><li>通过 IP 的其他表达形式可能绕过文本判断。</li><li>DNS 解析结果没有进行地址分类。</li><li>没有限制重定向，目标服务器可能返回另一个地址。</li><li>未限制响应体大小，容易造成内存或磁盘资源耗尽。</li><li>允许任意协议转换或 URL 解析行为，具体风险取决于客户端实现。</li><li>没有明确限制端口和出口网络范围。</li></ul><h2>四、正确的审计方法：验证“最终连接目标”</h2><p>防护的核心不是判断用户输入长什么样，而是确认客户端最终连接到什么地址。建议把目标解析为结构化对象，并对主机、端口、协议和解析结果分别检查。</p><p>在 Java 服务中，可以将校验逻辑拆成以下几个阶段：</p><pre><code>URI uri = URI.create(input);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();

if (!"https".equalsIgnoreCase(scheme) &amp;&amp; !"http".equalsIgnoreCase(scheme)) {
    throw new IllegalArgumentException("unsupported scheme");
}

if (host == null || host.isBlank()) {
    throw new IllegalArgumentException("invalid host");
}

if (port != -1 &amp;&amp; port != 80 &amp;&amp; port != 443) {
    throw new IllegalArgumentException("unsupported port");
}</code></pre><p>这只是基础校验，不能单独作为完整防护。之后还需要解析主机名，并对每个解析结果进行地址分类。对于业务不需要访问内网资源的服务，通常应拒绝回环地址、链路本地地址、私有地址、未指定地址以及其他保留地址。</p><pre><code>InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
    if (address.isAnyLocalAddress()
            || address.isLoopbackAddress()
            || address.isLinkLocalAddress()
            || address.isSiteLocalAddress()) {
        throw new IllegalArgumentException("destination is not allowed");
    }
}</code></pre><p>实际项目中不建议仅依赖这几个便捷方法。应结合组织的网络策略，对 IPv4、IPv6、云平台地址段、办公网地址段和容器网络地址段进行统一封装，并为该组件配套单元测试和回归测试。</p><h2>五、DNS 重绑定与校验时机问题</h2><p>一个经常被忽略的问题是：校验阶段解析到的地址，不一定是连接阶段使用的地址。若应用先解析域名并判断结果安全，随后由 HTTP 客户端再次解析域名，就存在两次解析结果不一致的可能。</p><p>因此，较稳妥的做法包括：</p><ul><li>在受控网络环境中使用固定的出口代理，由代理统一执行最终连接和地址策略。</li><li>使用能够固定解析结果或提供自定义 DNS 解析器的 HTTP 客户端。</li><li>在建立连接前后都执行地址检查，并避免客户端自动重新解析未经校验的主机名。</li><li>通过网络层策略阻断应用容器访问不必要的内网网段，不能只依赖业务代码。</li></ul><p>在审计报告中，建议明确写出“校验点”和“连接点”是否一致，而不是笼统描述为“存在 DNS 绕过”。这样更便于开发人员定位客户端配置和网络出口问题。</p><h2>六、重定向是第二个高频风险点</h2><p>即使初始 URL 通过了校验，服务端也可能返回 3xx 响应，引导客户端访问另一个地址。如果 HTTP 客户端默认跟随重定向，那么真正的访问目标已经发生变化。</p><p>安全处理方式有两种：</p><ul><li>业务不需要重定向时，直接关闭自动跟随，并将 3xx 作为失败处理。</li><li>业务确实需要重定向时，对每一跳的目标重新解析、重新校验，并限制最大跳转次数。</li></ul><pre><code>HttpClient client = HttpClient.newBuilder()
        .followRedirects(HttpClient.Redirect.NEVER)
        .connectTimeout(Duration.ofSeconds(3))
        .build();</code></pre><p>对于下载器、图片代理等功能，建议把重定向次数限制在较小范围内，并限制每次跳转只能使用允许的协议和端口。不要仅校验初始地址。</p><h2>七、响应内容同样属于攻击面</h2><p>SSRF 的影响不只来自“能否访问某个地址”，还取决于应用如何处理响应。常见的二次风险包括：</p><ul><li>读取内部接口返回的配置、调试信息或身份凭据。</li><li>将响应内容直接回显给前端，形成信息泄露。</li><li>把响应当作 XML 解析，引入 XXE 风险。</li><li>把响应当作图片、压缩包或文档处理，触发解析器漏洞。</li><li>无限读取大响应，导致内存、磁盘或线程资源耗尽。</li></ul><p>因此，远程资源获取功能至少应设置连接超时、读取超时、最大响应大小、最大重定向次数和允许的 Content-Type。不要只根据响应头判断安全，服务端返回的头部本身也可能不可信，必要时应对实际内容做有限度的格式识别。</p><pre><code>long maxBytes = 5 * 1024 * 1024;
try (InputStream in = response.body()) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buffer = new byte[8192];
    long total = 0;
    int n;
    while ((n = in.read(buffer)) != -1) {
        total += n;
        if (total &gt; maxBytes) {
            throw new IOException("response too large");
        }
        out.write(buffer, 0, n);
    }
}</code></pre><h2>八、在隔离环境中进行安全验证</h2><p>验证 SSRF 时不应直接探测生产内网，也不应访问真实的管理接口或云平台元数据服务。更推荐建立最小化实验环境：</p><ul><li>使用本地容器启动一个业务服务和一个仅用于记录请求的测试服务。</li><li>让测试服务返回固定的非敏感标记，例如 <code>TEST-SSRF-OK</code>。</li><li>通过应用日志、容器网络日志和代理日志确认实际请求目标。</li><li>对重定向、超时、超大响应和不同地址类型分别编写自动化测试。</li></ul><p>例如，可以在本地测试服务中记录请求来源、Host、路径和 User-Agent，再由业务服务访问该测试地址。测试的重点是确认请求是否按照预期被限制，而不是尝试读取任何真实敏感信息。</p><pre><code>curl --max-time 3  SSRF，合理的身份权限也能降低进一步影响。</p><h2>十、代码审计中的排查清单</h2><ul><li>搜索所有由用户输入决定目标地址的代码路径，而不只是搜索 HTTP 客户端调用。</li><li>检查 URL 是否经过结构化解析，避免纯字符串黑名单。</li><li>确认是否校验协议、主机、端口和解析后的全部地址。</li><li>检查 IPv4、IPv6、域名多解析结果以及重定向处理。</li><li>确认 DNS 校验与实际连接是否使用同一套解析结果。</li><li>检查连接、读取、响应大小和重定向次数限制。</li><li>确认响应是否进入 XML、图片、压缩包、模板或脚本解析流程。</li><li>检查日志中是否包含完整 URL、Authorization 头或敏感响应内容。</li><li>结合容器、主机、防火墙和云网络策略判断真实可达范围。</li><li>为修复方案补充回归测试，避免后续重构重新引入问题。</li></ul><h2>十一、修复后的回归测试设计</h2><p>回归测试不应只验证一个被拦截的地址，而应覆盖整个请求状态机。可以至少准备以下测试类别：</p><ul><li>合法的业务域名和合法端口能够正常访问。</li><li>回环、私有、链路本地和未指定地址被拒绝。</li><li>主机名解析得到多个地址时，只要存在不允许地址就拒绝。</li><li>初始地址合法、重定向目标不合法时，请求被终止。</li><li>响应超过大小限制时，连接被安全关闭。</li><li>连接超时和读取超时不会长期占用工作线程。</li><li>不支持的协议和端口无法进入底层客户端。</li><li>错误日志包含足够的排查信息，但不泄露敏感响应内容。</li></ul><h2>总结</h2><p>SSRF 审计的关键不是寻找某个“万能黑名单”，而是建立清晰的请求边界：谁提供目标地址，应用解析出了什么，最终连接到了哪里，响应又被如何处理。比较可靠的治理方式通常由四部分组成：结构化解析与严格校验、对 DNS 和重定向保持一致控制、限制响应处理资源，以及通过代理和网络隔离补充应用层防护。</p><p>从工程实践看，最值得优先改进的往往不是某一行过滤代码，而是把远程请求能力集中到一个经过审计的组件中，统一处理协议、地址、端口、超时、大小和日志策略。这样既能降低修复成本，也能避免不同业务各自实现一套不完整的 SSRF 防护逻辑。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-cloud.svg" alt="服务器与云环境安全示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">服务器、云资产与权限边界梳理示意</figcaption></figure>]]></description><guid isPermaLink="false">282</guid><pubDate>Sun, 11 Oct 2026 22:55:21 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21;&#x6587;&#x4EF6;&#x4E0A;&#x4F20;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x770B; Web &#x98CE;&#x9669;&#x6536;&#x655B;&#x601D;&#x8DEF;</title><link>https://www.hackhat.org/blogs/entry/281-%E4%BB%8E%E4%B8%80%E6%AC%A1%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E7%9C%8B-web-%E9%A3%8E%E9%99%A9%E6%94%B6%E6%95%9B%E6%80%9D%E8%B7%AF/</link><description><![CDATA[<h2>背景</h2><p>文件上传是 Web 系统里很常见的功能：头像、附件、工单截图、合同文档、插件包导入等都会涉及。也正因为它离用户输入很近，一旦校验和存储策略处理不好，风险往往不是单点问题，而是会串到解析、访问控制、存储桶权限、静态资源服务和后端任务处理链路。</p><p>这篇文章整理的是我在日常代码审计和漏洞复现中比较常用的一套检查方法，重点放在“如何识别风险”和“如何把风险收敛掉”。不讨论非法利用，也不提供面向攻击的操作路径，更多是给开发、安全同学做自查时参考。</p><h2>典型场景</h2><p>某业务系统提供附件上传能力，后端使用 Spring Boot，文件最终落到本地磁盘或对象存储，数据库中保存文件名、访问路径、上传人、业务类型等信息。常见代码大概长这样：</p><pre><code>@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) throws IOException {
    String originalName = file.getOriginalFilename();
    String path = uploadDir + "/" + originalName;
    file.transferTo(new File(path));
    return Result.ok("/files/" + originalName);
}</code></pre><p>这类代码看起来能跑，但安全边界非常薄。审计时一般不会只看“能不能上传某类危险文件”，而是会沿着完整链路看：文件名如何处理、类型如何识别、保存位置是否可执行、访问 URL 是否受控、是否存在覆盖、是否可能被后端解析、是否有异步任务处理文件内容。</p><h2>问题拆解</h2><p>文件上传相关风险可以拆成几个层面：</p><ul><li><strong>文件名风险：</strong>目录穿越、特殊字符、同名覆盖、日志污染、前端展示 XSS。</li><li><strong>类型校验风险：</strong>只检查后缀、只检查 Content-Type、黑名单不完整、双后缀误判。</li><li><strong>存储风险：</strong>上传目录位于 Web 可执行路径下，静态资源服务器直接暴露原始文件。</li><li><strong>解析风险：</strong>图片处理、压缩包解压、Office/PDF 解析、病毒扫描组件本身可能触发漏洞。</li><li><strong>权限风险：</strong>附件 URL 可枚举，跨用户访问，临时文件长期有效。</li><li><strong>资源消耗风险：</strong>超大文件、压缩炸弹、海量小文件、慢上传拖垮连接。</li></ul><p>很多事故不是某一个校验没写，而是多个弱点叠加。例如：后缀校验通过、文件存储在静态目录、访问路径可预测、没有鉴权，最后导致任意用户都能访问他人附件。</p><h2>代码审计关注点</h2><h2>1. 原始文件名不应直接参与存储路径</h2><p>审计时首先搜索 MultipartFile、upload、transferTo、FileOutputStream、Files.copy 等关键点。重点看 originalFilename 是否被直接拼接到路径中。</p><pre><code>// 风险写法：直接使用用户提交的文件名
String originalName = file.getOriginalFilename();
File target = new File(uploadDir + "/" + originalName);
file.transferTo(target);</code></pre><p>推荐做法是：业务展示名和真实存储名分离。真实存储名由服务端生成，展示名只作为普通字段保存，并在展示时做 HTML 转义。</p><pre><code>String originalName = StringUtils.cleanPath(file.getOriginalFilename());
String ext = getSafeExtension(originalName);
String storageName = UUID.randomUUID().toString().replace("-", "") + ext;
Path target = Paths.get(uploadDir).resolve(storageName).normalize();

if (!target.startsWith(Paths.get(uploadDir).normalize())) {
    throw new IllegalArgumentException("invalid path");
}

Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);</code></pre><p>这里有两个点比较关键：一是 normalize 后检查路径是否仍在上传目录内，二是不要让用户决定真实落盘文件名。</p><h2>2. 类型校验要使用白名单，而不是黑名单</h2><p>黑名单很容易漏，比如不同语言、不同服务器、不同中间件对扩展名和解析规则的理解不完全一致。更稳妥的是按业务要求设置白名单，比如头像只允许 jpg、png、webp，合同只允许 pdf、docx。</p><pre><code>private static final Set&lt;String&gt; ALLOWED_EXT = Set.of(".jpg", ".jpeg", ".png", ".webp", ".pdf");

private String getSafeExtension(String filename) {
    String lower = filename.toLowerCase(Locale.ROOT);
    int index = lower.lastIndexOf('.');
    if (index &lt; 0) {
        throw new IllegalArgumentException("missing extension");
    }
    String ext = lower.substring(index);
    if (!ALLOWED_EXT.contains(ext)) {
        throw new IllegalArgumentException("unsupported file type");
    }
    return ext;
}</code></pre><p>不过只看后缀仍然不够。Content-Type 也不能完全信任，因为它来自客户端请求头。更合适的是做多层判断：扩展名白名单、服务端 MIME 探测、必要时检查文件魔数。</p><pre><code>String detected = Files.probeContentType(target);
if (detected == null || !detected.startsWith("image/")) {
    Files.deleteIfExists(target);
    throw new IllegalArgumentException("invalid image content");
}</code></pre><p>需要注意，Files.probeContentType 在不同系统上的行为可能不同，不能作为唯一依据。对于图片类上传，通常还会使用 ImageIO 或专门库读取一次，确认确实能被安全解析。</p><h2>3. 上传目录不要具备执行能力</h2><p>比较稳的策略是：上传文件不放在应用部署目录下，更不要放在能被脚本引擎执行的位置。对外访问时通过下载接口或静态资源服务读取，统一加鉴权、限速和响应头控制。</p><pre><code># Nginx 示例：上传目录只作为静态文件读取，不启用任何脚本解析
location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    default_type application/octet-stream;
    add_header X-Content-Type-Options nosniff;
    add_header Content-Disposition "attachment";
}</code></pre><p>如果业务需要图片在线预览，可以按类型单独处理，不建议所有文件都 inline 展示。对于用户可控内容，响应头里加 X-Content-Type-Options: nosniff 是一个低成本但很有效的防线。</p><h2>4. 访问控制不能依赖“路径难猜”</h2><p>很多系统会把文件名改成 UUID，然后认为足够安全。UUID 可以降低枚举概率，但不等于权限控制。附件和业务对象通常存在归属关系，下载时应该检查当前用户是否有访问该业务对象的权限。</p><pre><code>@GetMapping("/files/{id}")
public ResponseEntity&lt;Resource&gt; download(@PathVariable Long id, Principal principal) {
    FileMeta meta = fileService.getMeta(id);
    if (!permissionService.canRead(principal.getName(), meta.getBizId())) {
        return ResponseEntity.status(403).build();
    }
    Resource resource = fileStorage.load(meta.getStorageName());
    return ResponseEntity.ok()
        .header("X-Content-Type-Options", "nosniff")
        .header("Content-Disposition", "attachment; filename=\"" + meta.getSafeDownloadName() + "\"")
        .body(resource);
}</code></pre><p>这里建议接口使用文件 ID，而不是直接暴露真实存储路径。真实路径、对象存储 key、bucket 名称等都不应成为前端可控参数。</p><h2>5. 解压和解析类功能要单独建模</h2><p>上传压缩包、导入 Excel、解析 PDF 这类功能风险更高。它们不只是“保存文件”，还会触发后端解析逻辑。审计时要重点看：</p><ul><li>解压是否限制目标目录，防止路径逃逸。</li><li>是否限制解压后文件数量、总大小、目录层级。</li><li>是否限制单个文件大小和压缩比。</li><li>解析库版本是否存在已知漏洞。</li><li>解析过程是否放在低权限、隔离环境中。</li></ul><p>解压时至少要做 canonical path 检查，确保每个解压出的文件都在目标目录内。</p><pre><code>Path destDir = Paths.get("/data/import/workdir").toRealPath();

Path out = destDir.resolve(entry.getName()).normalize();
if (!out.startsWith(destDir)) {
    throw new SecurityException("zip entry path traversal detected");
}</code></pre><p>同时要限制解压规模，避免被异常压缩包拖垮磁盘或 CPU。这个点在内网系统里很容易被忽略。</p><h2>关键加固清单</h2><table><tr><th>检查项</th><th>建议做法</th></tr><tr><td>文件名</td><td>服务端生成存储名，原始名仅用于展示并转义</td></tr><tr><td>类型校验</td><td>扩展名白名单 + MIME 探测 + 必要的内容识别</td></tr><tr><td>存储位置</td><td>放在非 Web 执行目录，避免脚本解析</td></tr><tr><td>访问控制</td><td>下载接口绑定业务权限，不依赖路径不可猜</td></tr><tr><td>响应头</td><td>设置 nosniff，按需设置 attachment</td></tr><tr><td>资源限制</td><td>限制单文件大小、总量、频率、压缩包解压规模</td></tr><tr><td>对象存储</td><td>默认私有桶，通过后端签发短期访问地址</td></tr><tr><td>日志审计</td><td>记录上传人、业务 ID、文件摘要、大小、类型、处理结果</td></tr></table><h2>Spring Boot 中的基础限制</h2><p>框架层可以先做一层基础限制，避免超大请求直接打到业务逻辑。示例配置如下：</p><pre><code>spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
server.tomcat.max-swallow-size=20MB</code></pre><p>这只是第一层保护，不能替代业务层的大小、类型、权限校验。特别是多个文件批量上传时，还要限制文件数量和用户级频率。</p><h2>审计时的排查路径</h2><p>实际看项目时，我一般按下面顺序走，效率会高一些：</p><ul><li>全局搜索上传入口：MultipartFile、RequestPart、transferTo、Files.copy、FileOutputStream。</li><li>确认文件元数据表：看存储名、原始名、业务 ID、用户 ID、访问路径如何设计。</li><li>追踪下载接口：看是否根据业务权限鉴权，是否存在按 path/key 直接读取。</li><li>检查静态资源配置：Nginx、Spring ResourceHandler、对象存储桶策略。</li><li>检查异步处理：缩略图、OCR、解压、格式转换、病毒扫描等任务。</li><li>检查异常处理：失败文件是否残留，临时目录是否定期清理。</li></ul><p>如果只能做快速基线，优先看三件事：是否直接使用原始文件名、上传目录是否可执行、下载是否缺权限校验。这三类问题命中率较高，影响也比较直接。</p><h2>一些容易被忽略的细节</h2><ul><li><strong>前端限制不算安全控制：</strong>accept 属性、JS 校验只能提升体验，不能作为后端信任依据。</li><li><strong>对象存储公有读要谨慎：</strong>公有读一旦配错，历史附件可能被批量访问。更建议私有桶 + 短期签名 URL。</li><li><strong>文件摘要有助于审计：</strong>保存 SHA-256 可以辅助去重、追踪异常文件和排查事件。</li><li><strong>临时目录也要管：</strong>很多系统只清理正式文件，不清理导入失败、解析失败的临时文件。</li><li><strong>错误信息不要暴露路径：</strong>异常返回里不要带服务器绝对路径、对象存储 key、内部桶名。</li></ul><h2>总结</h2><p>文件上传安全不是单个 if 判断能解决的问题。比较稳的思路是把它当成一条链路来治理：入口限制、类型识别、隔离存储、受控访问、解析防护、日志审计和定期清理都要覆盖。</p><p>从代码审计角度看，最值得优先修的是三点：不要直接使用用户文件名落盘，不要把上传目录放到可执行路径下，不要让附件访问绕过业务权限。把这几处收敛好，绝大多数常见上传风险都能降到可控范围。</p><blockquote>经验上，上传功能越“通用”，越需要边界清晰。不要为了复用一个上传接口，让头像、合同、导入包、插件包走同一套弱校验逻辑。按业务类型拆策略，反而更容易长期维护。</blockquote>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">281</guid><pubDate>Sun, 11 Oct 2026 22:53:04 +0000</pubDate></item><item><title>&#x4ECE;&#x4E00;&#x6B21;&#x540E;&#x53F0;&#x672A;&#x6388;&#x6743;&#x8BBF;&#x95EE;&#x770B;&#x63A5;&#x53E3;&#x9274;&#x6743;&#x4E0E;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x8981;&#x70B9;</title><link>https://www.hackhat.org/blogs/entry/280-%E4%BB%8E%E4%B8%80%E6%AC%A1%E5%90%8E%E5%8F%B0%E6%9C%AA%E6%8E%88%E6%9D%83%E8%AE%BF%E9%97%AE%E7%9C%8B%E6%8E%A5%E5%8F%A3%E9%89%B4%E6%9D%83%E4%B8%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E8%A6%81%E7%82%B9/</link><description><![CDATA[<h2>背景</h2><p>最近在做内部系统安全复盘时，遇到一个比较典型的问题：某业务后台已经接入统一登录，但部分管理接口仍然可以在未登录状态下被访问。问题本身不复杂，却很有代表性，涉及路由鉴权、前后端权限边界、接口设计习惯以及代码审计方法。</p><p>这类问题在中小型后台、快速迭代的运营系统、历史项目迁移中比较常见。前端页面可能已经做了登录拦截，菜单也按角色隐藏，但后端接口如果没有统一鉴权，实际风险仍然存在。</p><blockquote>本文只讨论防御视角下的漏洞分析和修复思路，不涉及针对真实目标的攻击利用过程。示例均为本地测试环境中的抽象代码。</blockquote><h2>问题场景</h2><p>系统大致结构如下：</p><ul><li>前端：Vue 单页应用，通过路由守卫判断是否存在 token。</li><li>后端：Spring Boot 提供 REST API。</li><li>登录：统一认证服务签发 JWT。</li><li>权限：部分接口使用注解校验，部分接口依赖网关或前端控制。</li></ul><p>问题接口位于后台管理模块，例如用户列表、配置查询、导出任务状态等接口。页面层面无法直接进入，但接口在缺少登录态时仍返回数据。</p><p>典型表现包括：</p><ul><li>访问管理页面会跳转登录页，但直接访问接口仍返回 200。</li><li>同一 Controller 中部分方法有权限注解，部分方法没有。</li><li>新增接口未纳入统一拦截器白名单/黑名单策略。</li><li>测试环境配置被复制到生产环境，导致部分路径被放行。</li></ul><h2>技术分析</h2><p>这类未授权访问通常不是单点代码失误，而是权限模型不清晰导致的工程问题。常见成因有以下几类。</p><h2>1. 只在前端做登录拦截</h2><p>前端路由守卫只能提升用户体验，不能作为安全边界。比如常见的写法：</p><pre><code>router.beforeEach((to, from, next) =&gt; {
  const token = localStorage.getItem('token')
  if (!token &amp;&amp; to.path !== '/login') {
    next('/login')
  } else {
    next()
  }
})</code></pre><p>这段逻辑只能阻止普通用户通过页面导航访问后台，无法约束后端 API。只要后端没有校验，接口仍然是暴露的。</p><h2>2. 后端鉴权策略不统一</h2><p>在 Spring Boot 项目中，经常看到两种混用方式：</p><ul><li>部分接口通过拦截器校验 token。</li><li>部分接口通过注解如 <code>@PreAuthorize</code> 控制权限。</li><li>少量历史接口完全没有接入权限体系。</li></ul><p>例如：</p><pre><code>@RestController
@RequestMapping("/admin/user")
public class AdminUserController {

    @PreAuthorize("hasAuthority('admin:user:list')")
    @GetMapping("/list")
    public List&lt;UserVO&gt; list() {
        return userService.listUsers();
    }

    @GetMapping("/detail")
    public UserVO detail(Long id) {
        return userService.getUserDetail(id);
    }
}</code></pre><p>上面 <code>/list</code> 有权限校验，但 <code>/detail</code> 没有。如果项目没有全局认证拦截，<code>/detail</code> 就可能成为未授权访问点。</p><h2>3. 白名单配置过宽</h2><p>另一个常见问题是认证拦截器或网关白名单配置过于宽泛：</p><pre><code>security:
  ignore-paths:
    - /login
    - /captcha
    - /actuator/**
    - /admin/**/export
    - /admin/config/**</code></pre><p>配置初衷可能是放行登录、验证码、健康检查，或者临时调试导出接口。但随着业务迭代，某些路径下新增了敏感接口，白名单规则没有同步收敛，最终导致过度放行。</p><p>尤其需要注意通配符规则，例如 <code>/admin/**</code>、<code>/**/export</code>、<code>/api/public/**</code>。这类配置短期方便，长期风险较高。</p><h2>4. 网关鉴权和服务鉴权边界不清</h2><p>有些系统将认证逻辑放在 API Gateway 或 Nginx 层，后端服务默认信任来自内网的请求。这种架构不是不能用，但必须保证：</p><ul><li>后端服务不能被外部直接访问。</li><li>网关转发时要传递可信身份信息。</li><li>服务端仍需对关键操作做权限校验。</li><li>内部调用也要有明确的认证边界。</li></ul><p>如果后端服务端口误暴露，或者测试环境与生产环境网络策略不一致，就容易出现绕过网关直接访问服务接口的问题。</p><h2>代码审计关键步骤</h2><p>针对这类问题，建议不要只靠手工点页面，而是结合路由、配置、注解和日志做系统性排查。</p><h2>1. 梳理接口清单</h2><p>优先从后端路由入手，导出所有 Controller 映射关系。Spring Boot 可以通过 Actuator 或启动日志辅助查看，生产环境不建议直接开放相关端点。</p><pre><code># 本地测试环境查看接口映射
curl  Actuator，也可以用 IDE 搜索：</p><ul><li><code>@RequestMapping</code></li><li><code>@GetMapping</code></li><li><code>@PostMapping</code></li><li><code>@PutMapping</code></li><li><code>@DeleteMapping</code></li></ul><p>重点关注路径中包含以下关键词的接口：</p><ul><li><code>admin</code>、<code>manage</code>、<code>system</code></li><li><code>user</code>、<code>role</code>、<code>permission</code></li><li><code>config</code>、<code>setting</code></li><li><code>export</code>、<code>download</code></li><li><code>log</code>、<code>audit</code></li></ul><h2>2. 对比鉴权注解覆盖情况</h2><p>可以用简单脚本或 IDE 结构视图检查 Controller 方法是否缺少鉴权注解。不是所有方法都必须写注解，但管理类接口至少应有统一认证兜底。</p><p>常见需要检查的注解包括：</p><ul><li><code>@PreAuthorize</code></li><li><code>@Secured</code></li><li><code>@RolesAllowed</code></li><li>项目自定义注解，例如 <code>@RequiresPermission</code>、<code>@AuthCheck</code></li></ul><p>一个相对稳妥的做法是：全局先校验登录态，再由方法级注解校验具体权限。也就是认证和授权分层处理。</p><h2>3. 检查拦截器与白名单</h2><p>重点看 WebMvcConfigurer、Filter、SecurityConfig、Gateway Filter 等位置。</p><pre><code>@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/login",
                        "/captcha",
                        "/health"
                );
    }
}</code></pre><p>这里的原则是：默认拦截，明确放行。放行路径越少越好，且需要定期审查。</p><p>不建议使用下面这种配置：</p><pre><code>.excludePathPatterns(
    "/admin/**",
    "/api/**/public/**",
    "/**/download/**"
);</code></pre><p>如果确实存在公开下载或公开查询接口，建议单独放在明确的 public Controller 下，避免与后台管理接口混用路径。</p><h2>4. 验证不同身份下的访问结果</h2><p>测试时至少准备三类请求：</p><ul><li>无 token 请求。</li><li>普通用户 token 请求。</li><li>管理员 token 请求。</li></ul><p>预期结果应当清晰：</p><table><tr><td>请求身份</td><td>后台列表接口</td><td>个人信息接口</td><td>公开验证码接口</td></tr><tr><td>无 token</td><td>401</td><td>401</td><td>200</td></tr><tr><td>普通用户</td><td>403</td><td>200</td><td>200</td></tr><tr><td>管理员</td><td>200</td><td>200</td><td>200</td></tr></table><p>这里要区分 401 和 403：</p><ul><li><code>401 Unauthorized</code>：未认证，缺少有效身份。</li><li><code>403 Forbidden</code>：已认证，但权限不足。</li></ul><p>很多系统统一返回 200 再用业务码表示失败，这种方式不是完全不可行，但会增加网关、监控和自动化测试的判断成本。新项目建议尽量使用标准 HTTP 状态码。</p><h2>修复建议</h2><h2>1. 建立默认拒绝策略</h2><p>认证策略建议采用默认拒绝，而不是默认放行。除登录、验证码、静态资源、健康检查等明确公开接口外，其余接口都必须先通过认证。</p><pre><code>http
  .authorizeHttpRequests(auth -&gt; auth
      .requestMatchers("/login", "/captcha", "/health").permitAll()
      .anyRequest().authenticated()
  );</code></pre><p>如果项目使用 Spring Security，这是比较清晰的基线。对于自研拦截器，也应保持类似逻辑。</p><h2>2. 关键接口增加方法级授权</h2><p>只校验登录态还不够，后台管理接口应继续校验角色或权限点。</p><pre><code>@PreAuthorize("hasAuthority('system:user:read')")
@GetMapping("/admin/user/detail")
public UserVO detail(@RequestParam Long id) {
    return userService.getUserDetail(id);
}</code></pre><p>如果权限点过细，也要避免走向另一个极端：权限命名混乱、无人维护。建议按资源和动作组织，例如：</p><ul><li><code>system:user:read</code></li><li><code>system:user:update</code></li><li><code>system:role:assign</code></li><li><code>audit:log:export</code></li></ul><h2>3. 对敏感数据做字段级控制</h2><p>未授权访问的危害往往来自返回数据过多。即使接口权限修好了，也建议检查返回对象，避免把内部字段直接暴露给前端。</p><p>例如用户详情接口不应返回：</p><ul><li>密码哈希</li><li>盐值</li><li>重置密码 token</li><li>内部备注</li><li>身份证、手机号等非必要敏感字段</li></ul><p>推荐使用专门的 VO/DTO，不要直接返回数据库实体。</p><pre><code>public class UserVO {
    private Long id;
    private String username;
    private String displayName;
    private String status;
}</code></pre><h2>4. 加入自动化安全回归</h2><p>这类问题很适合做自动化回归。可以在集成测试中覆盖关键接口的无 token、低权限、高权限访问结果。</p><pre><code>@Test
void adminApiShouldRejectAnonymousRequest() throws Exception {
    mockMvc.perform(get("/admin/user/list"))
            .andExpect(status().isUnauthorized());
}</code></pre><p>对历史项目而言，哪怕先覆盖核心管理接口，也比完全依赖人工测试更可靠。</p><h2>5. 日志与告警</h2><p>权限拦截失败不是只返回错误就结束了，建议记录必要日志，便于排查异常访问。日志内容应包含：</p><ul><li>请求路径</li><li>请求方法</li><li>用户 ID 或匿名标识</li><li>客户端 IP</li><li>失败原因：未登录、token 失效、权限不足</li></ul><p>注意不要在日志中记录完整 token、密码、验证码等敏感内容。</p><h2>注意点</h2><ul><li>不要把前端菜单隐藏当作权限控制。</li><li>不要长期保留临时白名单。</li><li>不要在多个地方重复实现互不一致的鉴权逻辑。</li><li>不要让测试环境安全配置直接进入生产。</li><li>不要默认信任内网请求，尤其是容器化和微服务环境。</li><li>不要把实体对象直接作为接口返回值。</li></ul><h2>一个较实用的排查清单</h2><table><tr><td>检查项</td><td>建议</td></tr><tr><td>接口是否默认需要登录</td><td>除明确公开接口外全部认证</td></tr><tr><td>后台接口是否有权限点</td><td>管理类接口增加方法级授权</td></tr><tr><td>白名单是否过宽</td><td>避免使用大范围通配符</td></tr><tr><td>网关是否是唯一入口</td><td>确认后端服务未直接暴露</td></tr><tr><td>返回字段是否过多</td><td>使用 DTO/VO 控制输出</td></tr><tr><td>是否有自动化测试</td><td>覆盖匿名、低权、高权访问</td></tr><tr><td>是否有审计日志</td><td>记录失败原因但避免敏感信息</td></tr></table><h2>总结</h2><p>后台未授权访问通常不是某个接口少写一行代码这么简单，而是权限体系缺少统一边界的表现。修复时不要只补单个接口，更应回到整体设计：默认拒绝、统一认证、细粒度授权、最小化返回字段、自动化回归。</p><p>从代码审计角度看，排查这类问题最有效的方法是把接口清单、鉴权配置、权限注解和实际访问结果放在一起对照。只看页面很容易漏，只看代码也可能忽略运行时配置。对于长期维护的业务系统，这套检查方式值得沉淀到发布流程中。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-network.svg" alt="网络流量与边界分析示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">网络请求、日志与边界流量分析示意</figcaption></figure>]]></description><guid isPermaLink="false">280</guid><pubDate>Sun, 11 Oct 2026 22:51:33 +0000</pubDate></item><item><title>&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x89D2;&#x5EA6;&#x590D;&#x76D8;&#x4E00;&#x6B21;&#x6587;&#x4EF6;&#x4E0A;&#x4F20;&#x98CE;&#x9669;&#x6392;&#x67E5;</title><link>https://www.hackhat.org/blogs/entry/279-%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E8%A7%92%E5%BA%A6%E5%A4%8D%E7%9B%98%E4%B8%80%E6%AC%A1%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E9%A3%8E%E9%99%A9%E6%8E%92%E6%9F%A5/</link><description><![CDATA[<h2>背景</h2><p>文件上传是 Web 系统里非常常见的功能，头像、附件、工单截图、导入模板都会涉及。它看起来只是业务能力，但在安全审计里经常是高风险入口：一旦校验逻辑不完整，可能引发任意文件写入、存储型 XSS、服务端解析异常、敏感文件覆盖等问题。</p><p>这篇文章整理一次比较典型的文件上传风险排查思路，重点放在代码审计和防护设计上。内容不会涉及未授权入侵或攻击利用细节，更多是从研发和安全评审角度，说明如何判断一个上传功能是否可靠。</p><h2>常见场景</h2><p>在实际项目里，文件上传通常存在几类实现方式：</p><ul><li>后端应用直接接收文件并保存到本地目录。</li><li>后端接收后转存到对象存储，例如 S3、OSS、COS、MinIO。</li><li>前端直传对象存储，后端只负责签发上传凭证。</li><li>上传后进入异步处理流程，例如图片压缩、文档解析、病毒扫描。</li></ul><p>不同实现方式风险点不完全一样，但核心问题基本围绕三个方面：文件内容是否可信、文件路径是否可控、文件访问是否安全。</p><h2>问题分析</h2><p>审计上传功能时，不建议只看扩展名判断。扩展名是用户可控输入，只能作为辅助信息。更稳妥的方式是从以下几个维度交叉验证。</p><h2>一、文件类型校验不能只依赖后缀</h2><p>很多历史代码会这样判断：</p><pre><code>String filename = file.getOriginalFilename();
if (filename.endsWith(".jpg") || filename.endsWith(".png")) {
    file.transferTo(new File(uploadDir, filename));
}</code></pre><p>这个逻辑的问题比较明显：文件名完全来自客户端，攻击者可以构造特殊名称；同时，后缀无法证明文件内容是真正的图片。</p><p>更可靠的做法是至少同时检查 MIME、文件头特征和实际解码结果。以图片为例，可以用标准图片库尝试解析，并限制宽高、大小、格式。</p><pre><code>BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
    throw new IllegalArgumentException("invalid image");
}

int width = image.getWidth();
int height = image.getHeight();
if (width &lt;= 0 || height &lt;= 0 || width &gt; 8000 || height &gt; 8000) {
    throw new IllegalArgumentException("invalid image size");
}</code></pre><p>注意，MIME 头也来自请求，不应单独作为信任依据。它的价值更多是辅助判断和记录日志。</p><h2>二、文件名和路径必须重新生成</h2><p>上传保存时，最容易被忽略的是路径拼接。以下写法在代码审计中比较常见：</p><pre><code>File target = new File(uploadDir + "/" + file.getOriginalFilename());
file.transferTo(target);</code></pre><p>这里存在两个问题：一是原始文件名可能包含路径分隔符；二是可能导致文件覆盖。安全做法是服务端生成随机文件名，并将用户原始文件名仅作为展示字段保存到数据库。</p><pre><code>String ext = detectSafeExtension(file);
String safeName = UUID.randomUUID().toString().replace("-", "") + ext;
Path basePath = Paths.get(uploadDir).toAbsolutePath().normalize();
Path targetPath = basePath.resolve(safeName).normalize();

if (!targetPath.startsWith(basePath)) {
    throw new IllegalArgumentException("invalid upload path");
}

Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);</code></pre><p>这里的关键点是 normalize 后再检查 startsWith，避免路径穿越导致文件落到预期目录之外。</p><h2>三、上传目录不要具备脚本执行能力</h2><p>如果上传目录位于 Web 根目录下，并且应用服务器会解析其中某些脚本类型，那么风险会被放大。更推荐的方式是：</p><ul><li>上传文件存储在 Web 根目录之外。</li><li>通过受控下载接口读取文件。</li><li>对象存储桶默认私有，访问通过短期签名 URL 或后端代理。</li><li>静态资源域名与主站域名隔离，降低 XSS 影响面。</li></ul><p>Nginx 可以显式禁止上传目录下的脚本解析。示例仅作思路参考：</p><pre><code>location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    add_header X-Content-Type-Options nosniff;
}

location ~* ^/uploads/.*\.(php|jsp|jspx|asp|aspx|cgi|pl)$ {
    return 403;
}</code></pre><p>如果业务允许公开访问图片，也建议设置准确的 Content-Type，并加上 X-Content-Type-Options: nosniff，避免浏览器进行内容嗅探。</p><h2>四、大小、数量和解压类文件要做限制</h2><p>上传功能不仅有代码执行类风险，也有资源消耗风险。例如超大文件、批量上传、压缩包炸弹、图片解析消耗过高等。实际防护里建议加入：</p><ul><li>单文件大小限制。</li><li>单用户或单 IP 上传频率限制。</li><li>总存储配额限制。</li><li>图片宽高和像素总量限制。</li><li>压缩包解压后的文件数量、总大小、目录层级限制。</li></ul><p>对于 ZIP 处理，不能简单相信压缩前大小。需要边解压边统计，超过阈值立刻中断。</p><pre><code>long totalSize = 0;
int fileCount = 0;

while ((entry = zipInputStream.getNextEntry()) != null) {
    if (entry.isDirectory()) {
        continue;
    }
    fileCount++;
    if (fileCount &gt; 1000) {
        throw new IllegalArgumentException("too many files");
    }

    long written = copyWithLimit(zipInputStream, outputStream, 10 * 1024 * 1024);
    totalSize += written;
    if (totalSize &gt; 100 * 1024 * 1024) {
        throw new IllegalArgumentException("zip too large");
    }
}</code></pre><h2>五、文件下载接口也要一起审计</h2><p>很多时候上传保存本身没有明显问题，但下载接口存在越权或任意文件读取。常见风险包括：</p><ul><li>通过 fileId 下载时没有校验当前用户权限。</li><li>通过 path 参数读取文件，导致路径穿越。</li><li>返回文件时 Content-Disposition 处理不当，引发响应头注入。</li><li>私有文件使用永久公开 URL，无法撤销访问。</li></ul><p>较稳妥的做法是下载接口只接收文件 ID，通过数据库查询真实存储路径，并校验业务权限。</p><pre><code>Attachment attachment = attachmentService.getById(fileId);
if (attachment == null || !permissionService.canRead(currentUser, attachment)) {
    throw new AccessDeniedException("no permission");
}

Path filePath = storage.resolve(attachment.getStorageName());
return download(filePath, attachment.getOriginalName());</code></pre><h2>关键审计步骤</h2><p>实际排查时，可以按下面的顺序进行，效率会比较高。</p><ul><li>定位所有上传入口，包括后台、接口、富文本、导入功能和移动端接口。</li><li>确认文件最终保存位置，是本地目录还是对象存储。</li><li>检查文件名是否由服务端生成，是否存在路径拼接。</li><li>检查类型校验是否只依赖后缀或 Content-Type。</li><li>检查上传目录是否可被 Web 服务器解析执行。</li><li>检查是否存在大小、频率、配额限制。</li><li>检查下载接口是否有权限校验和路径约束。</li><li>查看日志中是否记录上传人、文件 ID、大小、类型、存储路径、处理结果。</li></ul><h2>安全基线建议</h2><table><tr><td>检查项</td><td>建议做法</td></tr><tr><td>文件名</td><td>服务端随机生成，原始文件名仅用于展示</td></tr><tr><td>保存路径</td><td>固定目录，normalize 后校验路径边界</td></tr><tr><td>类型判断</td><td>后缀、文件头、解析结果多重校验</td></tr><tr><td>访问控制</td><td>私有文件必须鉴权，避免永久公开链接</td></tr><tr><td>执行权限</td><td>上传目录禁止脚本执行，尽量放在 Web 根目录外</td></tr><tr><td>资源限制</td><td>限制大小、数量、频率、解压后总量</td></tr><tr><td>日志审计</td><td>记录上传主体、文件信息、处理结果和异常原因</td></tr></table><h2>注意点</h2><blockquote><p>上传安全不是一个 if 判断能解决的问题，而是一组边界控制：入口校验、存储隔离、访问鉴权、解析限制和日志审计都要同时考虑。</p></blockquote><ul><li>不要把前端校验当作安全措施，前端只能改善用户体验。</li><li>不要信任原始文件名、MIME、后缀、压缩包内路径。</li><li>不要让上传文件和应用代码处在同一执行上下文。</li><li>涉及文档预览、图片处理时，要关注第三方解析库漏洞和补丁更新。</li><li>如果有杀毒或内容检测流程，建议异步处理并设置隔离状态，未检测通过前不要公开访问。</li></ul><h2>总结</h2><p>文件上传风险的根源在于“把不可信内容带入服务端存储和解析流程”。审计时不能只看能不能上传某个后缀，更要看文件从进入系统到被访问、被解析、被下载的完整链路。</p><p>比较可靠的方案是：服务端生成文件名，固定存储目录，做内容级校验，上传目录不具备执行能力，下载接口按业务权限控制，并对资源消耗做上限约束。这样即使某个单点校验出现遗漏，也不会轻易演变成高危问题。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">279</guid><pubDate>Sun, 11 Oct 2026 22:50:25 +0000</pubDate></item><item><title>SSRF &#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E0E;&#x9632;&#x62A4;&#xFF1A;&#x4ECE; URL &#x89E3;&#x6790;&#x5230;&#x51FA;&#x53E3;&#x63A7;&#x5236;&#x7684;&#x5B8C;&#x6574;&#x6392;&#x67E5;&#x65B9;&#x6CD5;</title><link>https://www.hackhat.org/blogs/entry/278-ssrf-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%8E%E9%98%B2%E6%8A%A4%EF%BC%9A%E4%BB%8E-url-%E8%A7%A3%E6%9E%90%E5%88%B0%E5%87%BA%E5%8F%A3%E6%8E%A7%E5%88%B6%E7%9A%84%E5%AE%8C%E6%95%B4%E6%8E%92%E6%9F%A5%E6%96%B9%E6%B3%95/</link><description><![CDATA[<p>服务端请求伪造（SSRF）并不是简单的“用户可控 URL”问题。真正决定风险的，通常是 URL 解析方式、DNS 解析行为、重定向处理、网络出口策略以及响应内容是否会返回给用户。很多修复只增加了一个内网 IP 判断，实际仍可能被 IPv6、十进制 IP、DNS 重绑定或重定向绕过。本文以代码审计和防守验证为主线，整理一套适用于 Java Web 服务的 SSRF 排查方法。示例均应在本地靶场或经授权的测试环境中进行，不直接访问生产内网、云平台元数据服务或其他未授权目标。一、常见业务场景与风险边界SSRF 通常出现在以下功能中：远程图片、头像、附件抓取；Webhook、回调地址或第三方通知配置；URL 预览、网页截图、Markdown 远程资源解析；导入远程 XML、JSON、RSS 或 OpenAPI 文档；代理下载、在线转码和远程健康检查。审计时不要只搜索 HttpClient 或 RestTemplate。更重要的是追踪数据流：用户输入是否进入 URL 构造函数，是否进入 DNS 解析、TCP 连接或 HTTP 客户端，响应是否被读取并返回给攻击者。判断 SSRF 的关键不是“是否发送了 HTTP 请求”，而是“请求的目标地址和请求内容是否被低可信输入影响，以及服务端网络权限是否超过了业务需要”。二、代码审计的入口与数据流可以先从高风险 API 入手，再回溯参数来源。Java 项目中常见的请求点包括：java.net.URL、java.net.URI；Apache HttpClient、OkHttp、WebClient、RestTemplate；文件下载工具、图片解析器和 HTML 渲染器；自定义的 download、fetch、proxy、callback 方法。建议将审计过程分为三步。第一步确认输入来源，例如 HTTP 参数、JSON 字段、数据库中的管理员配置或消息队列内容。第二步确认是否存在规范化和校验。第三步确认真正建立连接时使用的地址是否仍然来自原始输入。String target = request.getParameter(&amp;quot;url&amp;quot;);<br />
URL url = new URL(target);<br />
HttpURLConnection connection = (HttpURLConnection) url.openConnection();<br />
connection.setConnectTimeout(3000);<br />
connection.setReadTimeout(5000);<br />
InputStream input = connection.getInputStream();上面的代码存在多个审计疑点：没有限制协议；没有校验主机；没有处理重定向；DNS 解析和实际连接之间可能发生变化；读取响应时也没有限制大小。即使增加一个简单的字符串判断，也不能认为问题已经解决。三、URL 校验中最容易忽略的问题1. 不要使用字符串前缀判断以下做法都不可靠：if (url.startsWith(&amp;quot; {<br />
    // 允许访问<br />
}字符串比较无法正确区分主机、端口、用户名信息和路径。例如，可信域名后面附加其他主机名、使用用户信息字段或改变大小写，都可能导致判断结果与实际连接目标不一致。应当先使用标准解析器解析，再对结构化字段进行校验。2. 只允许业务需要的协议如果业务只是抓取网页，通常只需要 http 和 https。应显式拒绝 file、jar、ftp、gopher 等不必要协议，同时注意底层 HTTP 客户端或第三方库是否支持自定义协议处理器。URI uri = URI.create(input);<br />
String scheme = uri.getScheme();<br />
if (!&amp;quot;http&amp;quot;.equalsIgnoreCase(scheme) &amp;amp;&amp;amp;<br />
    !&amp;quot;https&amp;quot;.equalsIgnoreCase(scheme)) {<br />
    throw new IllegalArgumentException(&amp;quot;unsupported scheme&amp;quot;);<br />
}3. 校验主机名，而不是只校验解析结果对于固定合作方，最稳妥的方式是使用允许列表，例如允许的完整域名、端口和路径范围。对于必须支持任意公网地址的场景，至少需要解析主机名并拒绝回环地址、链路本地地址、私有地址、保留地址和未指定地址。需要同时考虑 IPv4 和 IPv6。常见错误包括只判断 127.0.0.1，却没有覆盖 IPv6 回环地址；只判断字符串形式的私网网段，却没有处理不同格式的 IP 表示；只校验一次 DNS 结果，却在后续连接阶段重新解析。4. 注意 DNS 重绑定和解析时序典型的脆弱流程是：程序先解析域名并判断为公网地址，随后 HTTP 客户端再次解析同一域名并建立连接。如果两次解析结果不同，校验和连接使用的就不是同一个地址。防守侧可以采用以下策略：在解析阶段获取所有 A 和 AAAA 记录，逐一进行地址分类；连接时使用已经校验过的地址，避免再次根据原始域名解析；通过受控 DNS 解析器和网络出口策略做第二层限制；对重定向后的每个目标重新执行完整校验。在 Java 中，直接调用 InetAddress.getByName 并不能自动解决时序问题。关键在于校验后的地址是否真正被连接层使用，以及客户端是否会在内部重新解析域名。四、重定向是经常被遗漏的第二跳很多 HTTP 客户端默认跟随 301、302、307 或 308 重定向。如果只校验初始 URL，服务端可能先访问一个允许的站点，再被重定向到不应访问的地址。建议关闭自动重定向，由业务代码逐跳处理：HttpClient client = HttpClients.custom()<br />
    .disableRedirectHandling()<br />
    .setDefaultRequestConfig(<br />
        RequestConfig.custom()<br />
            .setConnectTimeout(3000)<br />
            .setResponseTimeout(5000)<br />
            .build())<br />
    .build();收到重定向响应后，应读取 Location，解析为绝对地址或基于原地址规范化后的新地址，然后重新执行协议、主机、端口和 IP 分类检查。重定向次数也应设置上限，通常一到三次已经足够。五、推荐的防护分层SSRF 不应只依赖一段业务代码修复，建议建立多层控制。第一层：业务允许列表能使用固定目标列表时，不要开放任意 URL。允许列表应至少包含协议、主机、端口，必要时还应限制路径。域名匹配要使用完整标签边界，不能简单使用后缀匹配。boolean allowedHost(String host) {<br />
    return &amp;quot;api.partner.example&amp;quot;.equalsIgnoreCase(host)<br />
        || &amp;quot;storage.partner.example&amp;quot;.equalsIgnoreCase(host);<br />
}第二层：地址分类与连接前校验对域名解析得到的每个地址进行分类，拒绝回环、私有、链路本地、组播、保留和未指定地址。还要限制端口范围，避免业务只需要 443 却允许访问任意内部服务端口。第三层：HTTP 客户端限制禁用不需要的协议和重定向；设置连接、读取和连接池超时；限制响应头和响应体大小；限制单次请求的最大下载时间；禁止将内部请求凭据自动转发到外部目标。第四层：网络出口控制在容器、虚拟机或主机防火墙层面限制服务的出站访问，只允许业务所需的 DNS、代理和目标网段。网络策略是代码防护失效后的最后一道边界，也能降低 SSRF 进一步访问内部管理面板、消息队列和数据库的风险。六、一个更完整的校验思路下面的代码是审计和设计时的简化示意，不建议未经评估直接复制到生产环境。生产实现还需要处理 DNS 缓存、IPv6、代理配置、连接复用和客户端内部解析行为。URI uri = URI.create(input);</p>
<p>if (uri.getUserInfo() != null) {<br />
    throw new IllegalArgumentException(&amp;quot;userinfo is not allowed&amp;quot;);<br />
}</p>
<p>String scheme = uri.getScheme();<br />
if (!&amp;quot;http&amp;quot;.equalsIgnoreCase(scheme)<br />
        &amp;amp;&amp;amp; !&amp;quot;https&amp;quot;.equalsIgnoreCase(scheme)) {<br />
    throw new IllegalArgumentException(&amp;quot;scheme is not allowed&amp;quot;);<br />
}</p>
<p>String host = uri.getHost();<br />
if (host == null || host.length() == 0) {<br />
    throw new IllegalArgumentException(&amp;quot;invalid host&amp;quot;);<br />
}</p>
<p>int port = uri.getPort();<br />
if (port != -1 &amp;amp;&amp;amp; port != 80 &amp;amp;&amp;amp; port != 443) {<br />
    throw new IllegalArgumentException(&amp;quot;port is not allowed&amp;quot;);<br />
}</p>
<p>for (InetAddress address : DnsResolver.resolveAll(host)) {<br />
    if (AddressPolicy.isPrivateOrReserved(address)) {<br />
        throw new IllegalArgumentException(&amp;quot;address is not allowed&amp;quot;);<br />
    }<br />
}实际连接时，还要确保 HTTP 客户端使用的是已校验的解析结果，或者通过受控代理统一发起请求。若无法保证客户端不会重新解析域名，应优先采用出口代理方案，并在代理层再次执行目标策略。七、响应处理同样需要安全边界SSRF 风险不只在“请求发到哪里”，还在于响应如何被处理。建议关注以下问题：限制响应体大小，防止大文件导致内存或磁盘耗尽；不要直接把内部服务响应原样返回给前端；按业务需要限制响应内容类型，图片抓取不应接受任意脚本或可执行格式；下载到临时目录时使用随机文件名，并设置权限和生命周期；对压缩文件、XML 和图片解析设置资源限制，避免引入其他解析类漏洞；记录目标域名、最终地址、状态码、响应大小和耗时，但不要记录敏感响应正文。如果业务只是判断 URL 是否可用，可以只读取响应头或少量内容，不必将整个响应加载到内存。对于异步抓取任务，应增加任务级超时、并发上限和失败重试上限。八、审计与验证清单检查项关注点输入来源参数、JSON、数据库配置、消息队列是否可被低可信用户影响协议限制是否只允许 http/https，是否存在自定义协议处理器主机校验是否使用结构化解析，是否覆盖 IPv4、IPv6 和域名DNS 行为校验和连接是否可能发生二次解析，是否考虑解析结果变化重定向是否关闭自动跟随，跳转后的目标是否重新校验端口控制是否限制到业务需要的端口，是否存在任意端口访问响应处理是否限制大小、类型、超时，并避免直接回显内部内容网络策略服务是否具备不必要的内网和管理网段出站权限九、在测试环境中的验证方法验证时建议搭建两个本地服务：一个作为允许访问的正常站点，另一个作为重定向站点或记录请求的观察端。测试目标是确认校验逻辑是否按预期拒绝不允许的协议、地址、端口和跳转，而不是扫描真实内网。python3 -m http.server 8080 --bind 127.0.0.1可以在单元测试中覆盖以下类别：合法公网域名、明确的本地回环地址、IPv6 回环地址、私有地址、无效端口、带用户名信息的 URL、初始地址合法但重定向地址不合法的场景，以及超大响应和长时间不返回的场景。测试结果应验证请求是否被阻断、日志是否完整、服务是否能够及时释放连接。十、日志、告警与事件复盘对可能发起外部请求的服务，建议记录结构化审计日志，包括请求来源、业务接口、目标主机、解析地址、最终地址、端口、状态码、耗时、响应大小和拒绝原因。日志中的查询参数和响应内容应进行脱敏。告警可以关注短时间内大量不同目标、访问异常端口、频繁触发地址策略拒绝、请求耗时明显升高以及服务向不常见公网网段发起连接等情况。复盘时要同时检查代码、依赖库默认行为、容器网络策略和云环境出站权限，避免只修补某一个 URL 参数。总结SSRF 的有效防护需要把 URL 解析、DNS、连接建立、重定向、响应处理和网络出口看成一条完整链路。单独增加黑名单、判断字符串前缀或只拦截某个内网地址，都很难覆盖真实环境中的边界情况。更可靠的实践是：优先使用业务允许列表；严格限制协议、主机和端口；对所有解析结果执行地址分类；关闭自动重定向并逐跳校验；限制响应资源；最后通过代理和网络策略建立出口边界。代码审计时沿着“输入来源—URL 构造—解析—连接—响应”的数据流检查，通常比单纯搜索危险函数更容易发现完整问题。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">278</guid><pubDate>Sun, 11 Oct 2026 22:49:12 +0000</pubDate></item><item><title>&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x5230;&#x7EB5;&#x6DF1;&#x9632;&#x5FA1;&#xFF1A;&#x4E00;&#x6B21; SSRF &#x98CE;&#x9669;&#x5206;&#x6790;&#x4E0E;&#x4FEE;&#x590D;&#x5B9E;&#x8DF5;</title><link>https://www.hackhat.org/blogs/entry/277-%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%88%B0%E7%BA%B5%E6%B7%B1%E9%98%B2%E5%BE%A1%EF%BC%9A%E4%B8%80%E6%AC%A1-ssrf-%E9%A3%8E%E9%99%A9%E5%88%86%E6%9E%90%E4%B8%8E%E4%BF%AE%E5%A4%8D%E5%AE%9E%E8%B7%B5/</link><description><![CDATA[<p>服务端请求伪造（SSRF）并不只是一个“能否访问内网”的简单问题。实际审计中，风险通常分散在 URL 解析、DNS 解析、重定向、代理配置和网络出口策略等多个环节。单纯增加一个正则表达式，往往只能拦住最直观的测试用例，却无法覆盖 IPv6、DNS 重绑定、编码差异以及跳转链等场景。</p><p>本文以一个常见的“远程图片抓取”功能为例，整理从代码定位、风险确认、修复设计到上线验证的完整思路。示例仅用于本地测试环境和防御性审计，不涉及访问真实内网服务或敏感信息。</p><h2>一、问题背景与典型场景</h2><p>很多业务会提供类似功能：用户提交一个图片或文档 URL，后端下载资源后进行转码、缓存或内容审核。伪代码通常如下：</p><pre><code>def fetch_resource(url):
    response = http_client.get(url, timeout=10)
    return response.content</code></pre><p>如果这个 URL 完全由用户控制，服务端就可能成为一个网络请求代理。风险主要包括：</p><ul><li>访问本不应由公网用户触达的管理接口、监控接口或内部服务。</li><li>将内网服务的响应内容带回前端，造成信息泄露。</li><li>借助重定向、代理或特殊地址表示绕过简单的域名黑名单。</li><li>消耗服务端连接、带宽和线程资源，形成应用层资源滥用。</li></ul><p>需要注意的是，SSRF 是否构成高危，取决于网络分区、服务权限、响应处理方式和出口策略。一个只能访问固定公网域名的抓取服务，与一个可以访问整个内部网络的服务，实际影响完全不同。</p><h2>二、代码审计时的定位方法</h2><p>审计时不要只搜索“requests.get”或“HttpClient”。更有效的方法是从用户可控输入开始，沿数据流追踪到网络请求函数。</p><ol><li>查找输入来源：HTTP 参数、JSON 字段、文件内容、Webhook 地址、Markdown 链接以及任务配置。</li><li>确认是否经过拼接或二次编码，例如 URL 被放入代理参数、重定向参数或 HTML 解析流程。</li><li>追踪最终请求函数，记录是否允许自动跳转、是否使用系统代理、是否验证 TLS、是否设置超时。</li><li>检查响应是否会被原样返回、写入日志、保存到对象存储，或交给后续解析器处理。</li><li>确认运行环境的网络权限，包括容器网段、云平台元数据访问、服务账号权限和出站防火墙策略。</li></ol><p>在审计记录中，建议同时保留“输入点—校验点—解析点—请求点—输出点”五个位置。很多漏洞并非没有校验，而是校验发生在错误的解析阶段，或者校验后的 URL 在后续流程中被重新解析。</p><h2>三、URL 校验中最容易出错的地方</h2><h3>1. 只做字符串匹配</h3><p>类似 <code>url.startswith(" 的判断不等价于域名校验。字符串前缀可能匹配到相似域名、用户名字段或路径内容。正确做法是使用标准 URL 解析器，分别检查 scheme、hostname、port 和 path。</p><h3>2. 只检查域名，不检查解析结果</h3><p>域名最终会解析为 IP 地址。即使域名本身看起来可信，也需要确认解析结果是否落入私网、回环、链路本地、保留地址或其他不允许的网络范围。IPv4 映射 IPv6 地址、十进制或十六进制地址表示，也不能依赖字符串规则判断。</p><h3>3. 忽略重定向</h3><p>初始地址可能是允许访问的公网地址，但服务端跟随 30x 跳转后，目标已经发生变化。因此需要关闭自动重定向，或者对每一次跳转重新执行完整校验，并限制最大跳转次数。</p><h3>4. 校验与连接之间存在时间差</h3><p>先 DNS 解析、再发起请求的流程存在 DNS 重绑定风险。一次解析得到公网 IP，并不意味着连接建立时仍然使用同一个地址。对高风险场景，应用层校验应配合固定出口代理、网络 ACL 和连接级地址控制，不能只依赖业务代码。</p><h2>四、一个防御性校验示例</h2><p>下面的代码展示了基础校验思路，适合作为审计或单元测试的起点。它不是完整的网络隔离方案，生产环境仍应通过受控代理和出站防火墙建立最后一道边界。</p><pre><code>from urllib.parse import urlsplit
import ipaddress
import socket

ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}

def host_resolves_to_public_ip(host):
    try:
        records = socket.getaddrinfo(
            host, None, type=socket.SOCK_STREAM
        )
    except socket.gaierror:
        return False

    if not records:
        return False

    for record in records:
        address = record[4][0]
        ip = ipaddress.ip_address(address)
        if not ip.is_global:
            return False
    return True

def validate_target(raw_url):
    parsed = urlsplit(raw_url)

    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        return False
    if not parsed.hostname:
        return False
    if parsed.username or parsed.password:
        return False

    try:
        port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
    except ValueError:
        return False

    if port not in ALLOWED_PORTS:
        return False
    if not host_resolves_to_public_ip(parsed.hostname):
        return False

    return True</code></pre><p>这个示例包含几个关键点：使用结构化解析而不是字符串拼接；拒绝用户名和密码字段；限制协议与端口；将域名解析为 IP 后检查地址属性；对解析失败和异常端口直接拒绝。</p><p>但需要特别强调，<code>getaddrinfo</code> 检查与后续 HTTP 连接之间仍然可能存在时间窗口。更稳妥的架构是让应用只向一个受控抓取代理发请求，由代理统一完成 DNS 解析、地址过滤、连接建立和重定向检查，业务服务本身不直接访问任意目标。</p><h2>五、请求层的安全控制</h2><p>URL 校验之外，请求客户端还应设置明确的资源边界：</p><ul><li>禁止或严格限制自动重定向，限制最大跳转次数。</li><li>设置连接超时、读取超时、总耗时和最大响应体大小。</li><li>限制响应内容类型，只允许业务真正需要的类型，例如图片或纯文本。</li><li>禁止将用户输入直接作为代理地址、请求头或认证信息。</li><li>对压缩响应、分块传输和异常 Content-Length 做大小保护，避免资源耗尽。</li><li>下载完成后重新检测文件类型，不要仅依据扩展名或响应头。</li></ul><p>以 Python 客户端为例，基础配置可以类似这样：</p><pre><code>response = session.get(
    target_url,
    allow_redirects=False,
    timeout=(3, 8),
    stream=True,
    headers={"Accept": "image/*"}
)

content_length = response.headers.get("Content-Length")
if content_length and int(content_length) &gt; 5 * 1024 * 1024:
    raise ValueError("response too large")</code></pre><p>实际读取响应体时，还需要在流式读取过程中累计字节数，不能只依赖 Content-Length，因为服务端可能不返回该字段，或者返回不可信的值。</p><h2>六、网络层的纵深防御</h2><p>代码修复解决的是应用层逻辑，网络层仍然需要独立控制：</p><ul><li>抓取服务使用独立网络区域，默认拒绝访问数据库、缓存、消息队列和管理网段。</li><li>通过出站防火墙或 egress proxy 只允许访问必要的公网端口和域名。</li><li>明确禁止访问回环、链路本地、私网、保留地址以及云环境敏感管理地址。</li><li>为抓取服务使用最小权限的服务账号，避免网络访问能力与云 API 权限叠加。</li><li>记录目标域名、解析结果、最终连接地址、响应状态、响应大小和耗时，便于追溯。</li></ul><p>日志中不要保存完整的认证 URL、Cookie 或可能包含敏感参数的请求头。对于用户提交的 URL，可以记录脱敏后的规范化结果和哈希值，既方便关联分析，也能减少二次泄露风险。</p><h2>七、如何在测试环境中验证修复</h2><p>验证应优先使用自建的多个测试服务，不要把测试请求指向生产内网或第三方基础设施。可以准备以下场景：</p><ul><li>正常公网 HTTPS 地址，确认业务功能不受影响。</li><li>回环地址、私网地址、链路本地地址和 IPv4 映射 IPv6 地址，确认被拒绝。</li><li>包含用户名字段、非标准端口和异常编码的 URL，确认解析行为一致。</li><li>返回 30x 的测试服务，确认跳转目标会重新校验。</li><li>返回超大响应、慢速响应和错误 Content-Type 的测试服务，确认超时与大小限制生效。</li><li>DNS 解析失败、返回多个地址或地址变化时，确认默认采取拒绝策略。</li></ul><p>建议把这些场景固化为单元测试和集成测试，尤其关注不同 URL 解析库之间的行为差异。升级运行时、HTTP 客户端或代理组件后，应重新执行这组测试。</p><h2>八、常见修复误区</h2><ul><li><strong>只维护黑名单：</strong>黑名单无法覆盖所有地址表示和未来新增网段，优先采用明确的协议、端口和目标范围白名单。</li><li><strong>只校验 hostname：</strong>必须结合 DNS 结果、重定向目标和实际连接地址判断。</li><li><strong>认为 HTTPS 可以解决 SSRF：</strong>HTTPS 只保护传输和证书校验，并不会阻止请求到达错误的网络位置。</li><li><strong>把解析逻辑散落在多个业务模块：</strong>应集中封装抓取能力，统一超时、大小、跳转和审计策略。</li><li><strong>只看 HTTP 状态码：</strong>响应体大小、内容类型、耗时和解析器行为同样可能造成安全问题。</li></ul><h2>九、总结</h2><p>SSRF 审计的重点不是寻找一个“万能正则”，而是完整理解从用户输入到网络连接的整个链路。可靠的防护通常由四部分组成：严格的 URL 结构校验、对解析结果和跳转目标的持续检查、受控的请求客户端配置，以及网络出口层的默认拒绝策略。</p><p>如果业务确实只需要抓取少量固定站点，最安全的方案通常不是允许任意 URL 后不断补丁，而是改成站点白名单或资源标识映射。对于必须支持动态目标的场景，也应优先采用隔离的抓取服务和受控代理，并通过日志与测试用例持续验证边界。这样才能把一次代码缺陷，降低为可监控、可限制、可回滚的工程风险。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">277</guid><pubDate>Sun, 11 Oct 2026 22:45:10 +0000</pubDate></item><item><title>&#x4ECE;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x5230;&#x4FEE;&#x590D;&#x9A8C;&#x8BC1;&#xFF1A;&#x4E00;&#x5957;&#x53EF;&#x843D;&#x5730;&#x7684; SSRF &#x98CE;&#x9669;&#x5206;&#x6790;&#x65B9;&#x6CD5;</title><link>https://www.hackhat.org/blogs/entry/276-%E4%BB%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%88%B0%E4%BF%AE%E5%A4%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E4%B8%80%E5%A5%97%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84-ssrf-%E9%A3%8E%E9%99%A9%E5%88%86%E6%9E%90%E6%96%B9%E6%B3%95/</link><description><![CDATA[<p>服务端请求伪造（SSRF）是代码审计中经常遇到的一类问题。它通常不是由某个危险函数单独造成，而是由“用户可控地址”“服务端发起请求”“缺少目标限制”三个条件共同形成。实际项目里，SSRF 可能出现在图片抓取、Webhook、远程文件导入、URL 预览、文档转换和代理接口等功能中。</p><p>这类问题的难点在于：简单的黑名单很容易被绕过，单纯判断字符串是否包含内网地址也不可靠，而修复时还要兼顾 DNS 解析、重定向、IPv6、代理配置和网络分区。本文以代码审计和防守验证为主线，整理一套适合实际项目的分析方法。</p><h2>一、先明确风险边界</h2><p>审计 SSRF 时，不要一开始就把所有“服务器发起的 HTTP 请求”都判定为漏洞。更准确的判断方式是分析请求目标是否受到攻击者控制，以及服务端能访问到什么网络区域。</p><ul><li><strong>高风险场景：</strong>接口允许用户提交任意 URL，服务端再将响应内容返回给用户，或将内容解析、保存、转发到其他系统。</li><li><strong>中风险场景：</strong>目标地址可控，但服务端有固定域名白名单；需要进一步确认白名单校验是否可靠。</li><li><strong>低风险场景：</strong>请求目标完全由服务端配置决定，用户只能选择有限的业务对象，且网络层也有访问控制。</li><li><strong>需要重点确认的影响：</strong>是否能够访问管理端口、内部 API、云平台实例元数据服务、容器管理接口或仅限本机访问的调试服务。</li></ul><p>风险评级不能只看“是否能请求 127.0.0.1”。同一个缺陷在开发环境可能只访问到测试接口，在生产环境则可能触达敏感管理平面，因此必须结合部署拓扑和出网策略判断。</p><h2>二、代码审计中的入口识别</h2><p>审计时可以从数据流入手，而不是只搜索某个固定函数名。常见入口包括以下几类：</p><ul><li>HTTP 参数、JSON 字段、表单字段中的 <code>url</code>、<code>uri</code>、<code>callback</code>、<code>webhook</code>、<code>source</code>。</li><li>上传后触发远程下载、图片转码、网页截图、PDF 转换的业务流程。</li><li>后台任务、消息队列消费者和定时任务中的远程资源同步逻辑。</li><li>支持自定义代理、远程仓库、镜像地址的管理功能。</li><li>将用户输入传入 HTTP 客户端、Socket、RPC、DNS 或命令执行组件的代码。</li></ul><p>搜索调用点时，除了常见的 HTTP 客户端，还要关注封装层。例如 Java 项目中的 <code>RestTemplate</code>、<code>WebClient</code>、OkHttp，Python 项目中的 <code>requests</code>、<code>urllib</code>、<code>httpx</code>，Node.js 项目中的 <code>axios</code>、<code>fetch</code>，以及项目自定义的 <code>HttpUtil</code>、<code>Downloader</code>、<code>UrlFetcher</code> 等类。</p><h2>三、建立完整的数据流</h2><p>一个可复现、可修复的审计结论，至少要说明以下数据流：</p><ol><li>用户输入从哪个参数进入系统。</li><li>输入是否经过 URL 解码、拼接、规范化或重定向。</li><li>最终由哪个组件发起请求。</li><li>请求使用了什么协议、端口、代理和超时配置。</li><li>响应内容是否返回给用户，或被写入文件、数据库、缓存。</li><li>服务端部署网络中有哪些可达目标。</li></ol><p>例如，下面这段代码的问题不在于使用了某个特定库，而在于完整 URL 由用户输入直接决定：</p><pre><code>String target = request.getParameter("url");
HttpURLConnection connection =
        (HttpURLConnection) new URL(target).openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(5000);
return readBody(connection.getInputStream());</code></pre><p>即使加入超时，也只能降低资源消耗，不能改变请求目标的可控性。若业务只需要抓取固定站点的公开资源，应该让用户提交业务标识或相对路径，由服务端根据配置生成最终地址，而不是直接接收完整 URL。</p><h2>四、为什么常见黑名单不可靠</h2><p>实际审计中经常看到以下防护方式：</p><pre><code>if (url.contains("127.0.0.1") || url.contains("localhost")) {
    throw new IllegalArgumentException("blocked");
}</code></pre><p>这种判断存在明显缺陷。地址可能使用不同的表示形式，域名还可能解析到受限制的地址，且初次解析和实际连接之间存在时间窗口。除此之外，IPv6、重定向、代理和 URL 解析差异也会造成过滤结果与实际连接目标不一致。</p><p>因此，不建议维护“危险地址字符串列表”。更稳妥的做法是采用正向允许策略，并在解析后的 IP 层面进行判断。防护逻辑至少应覆盖：</p><ul><li>只允许业务真正需要的协议，通常优先限制为 <code>https</code>，必要时再开放 <code>http</code>。</li><li>明确限制端口，避免允许任意端口访问。</li><li>解析所有 A、AAAA 记录，对每个结果进行内网、回环、链路本地、保留地址和组播地址检查。</li><li>连接前重新确认解析结果，避免 DNS 解析与连接之间的地址变化。</li><li>禁止或严格校验重定向，不能只校验第一次请求的目标。</li><li>关闭不必要的代理继承，避免环境变量或全局代理改变实际请求路径。</li><li>设置连接、读取、响应大小和总耗时限制。</li></ul><h2>五、推荐的安全请求流程</h2><p>如果业务确实需要访问用户提供的 URL，可以将请求过程拆成几个明确阶段。下面是伪代码，重点是流程而不是某个语言库的具体实现：</p><pre><code>URI uri = parseStrictly(input);
if (!isAllowedScheme(uri.getScheme())) {
    reject();
}
if (uri.getUserInfo() != null) {
    reject();
}
if (uri.getPort() != -1 &amp;&amp; !isAllowedPort(uri.getPort())) {
    reject();
}

List&lt;IPAddress&gt; addresses = dnsResolveAll(uri.getHost());
for (IPAddress address : addresses) {
    if (isPrivateOrReserved(address)) {
        reject();
    }
}

HttpClient client = buildClientWithoutProxy();
HttpResponse response = client.request(uri, new RequestLimits(
        connectTimeoutMs = 3000,
        readTimeoutMs = 5000,
        maxBodyBytes = 2 * 1024 * 1024,
        maxRedirects = 0
));

if (response.isRedirect()) {
    rejectOrRevalidateLocation(response.location());
}</code></pre><p>其中 <code>isPrivateOrReserved</code> 不应只判断 RFC1918 私有地址，还应覆盖回环地址、链路本地地址、未指定地址、组播地址、文档保留地址以及项目环境中明确不应访问的网段。对于 IPv6，要使用标准地址解析库判断，而不要通过字符串前缀匹配。</p><h2>六、DNS 与重定向是修复中的重点</h2><h3>1. DNS 解析问题</h3><p>域名白名单并不等于安全。某个域名在校验时解析到公开地址，实际连接时可能解析到不同地址；或者域名本身由用户控制，能够通过 DNS 配置指向内部网络。修复时应将“允许的主机名”和“解析后的 IP 校验”结合起来，并尽量在受控网络环境中使用固定解析或出站代理。</p><p>对于高安全等级场景，可以让应用不直接访问任意外部地址，而是统一调用一个出站代理。代理负责 DNS 解析、地址分类、审计记录和出站策略，应用只提交经过校验的目标。</p><h3>2. HTTP 重定向问题</h3><p>即使初始 URL 通过校验，服务器也可能返回 <code>Location</code>，将客户端引导到未授权地址。如果客户端自动跟随重定向，第一次校验就失去意义。</p><p>常见的处理方式有两种：</p><ul><li>业务不需要重定向时，直接关闭自动跟随，并将重定向视为失败。</li><li>业务必须支持重定向时，对每一次新的目标重新执行协议、主机、端口和 IP 校验，同时限制重定向次数。</li></ul><h2>七、在本地环境中进行安全验证</h2><p>漏洞验证应优先使用自建测试服务，不要对第三方系统、生产内网或云平台管理接口进行探测。可以准备一个简单的本地测试服务，记录收到的请求，并分别验证以下行为：</p><ul><li>正常公开域名是否可以按业务预期访问。</li><li>回环地址、私有地址和 IPv6 本地地址是否被拒绝。</li><li>域名解析到受限地址时是否被拒绝。</li><li>返回重定向到受限地址时，客户端是否停止或重新校验。</li><li>超大响应、慢连接和异常状态码是否受到限制。</li><li>URL 中包含用户名密码、非标准端口或不支持协议时是否被拒绝。</li></ul><p>在测试客户端层面，可以通过单元测试固定解析结果，避免依赖真实 DNS。示例测试命令仅用于验证本地服务的 HTTP 行为：</p><pre><code>curl --max-time 5 --connect-timeout 3 \
  -H 'Accept: application/json' \
  ' DNS 请求建立告警。</li><li>将图片抓取、文档转换等高风险任务放到隔离网络和低权限运行环境。</li></ul><p>网络层策略的价值在于降低漏洞影响范围，但它不能替代代码中的输入校验。因为应用可能通过代理、解析器、其他协议或内部服务间接访问受限资源。</p><h2>九、代码审计报告应如何落地</h2><p>一份有价值的报告不应只写“存在 SSRF”，建议至少包含以下内容：</p><table><tr><th>项目</th><th>说明</th></tr><tr><td>入口</td><td>接口、参数、权限要求及触发条件</td></tr><tr><td>数据流</td><td>从输入到 URL 解析、DNS、连接和响应处理的过程</td></tr><tr><td>影响</td><td>基于实际网络拓扑说明可访问的资源范围</td></tr><tr><td>根因</td><td>缺少正向校验、重定向未校验、代理配置不当等</td></tr><tr><td>修复建议</td><td>代码、网络、权限和日志层面的具体措施</td></tr><tr><td>验证结果</td><td>修复前后测试用例和预期行为</td></tr></table><p>修复验证不能只检查“某个字符串被拦截”。更合理的是覆盖一组行为测试，包括不同地址表示、多个 DNS 结果、重定向、异常端口、超时和超大响应。这样可以避免开发人员修复一个样例，却遗漏同一数据流中的其他路径。</p><h2>十、常见误区</h2><ul><li><strong>误区一：只禁止 localhost。</strong> 应检查解析后的地址类别，而不是只匹配一个主机名。</li><li><strong>误区二：只校验 URL 字符串。</strong> 字符串看起来安全，不代表最终连接地址安全。</li><li><strong>误区三：默认跟随重定向。</strong> 每次跳转都可能改变实际访问目标。</li><li><strong>误区四：只依赖云防火墙。</strong> 网络策略可能因环境变化而失效，应用仍需做最小权限控制。</li><li><strong>误区五：忽略非 HTTP 组件。</strong> 图片处理器、PDF 转换器、DNS 客户端和代理组件也可能形成类似风险。</li><li><strong>误区六：没有限制响应大小和耗时。</strong> 即使目标地址安全，也可能造成资源耗尽或任务堆积。</li></ul><h2>总结</h2><p>SSRF 的审计重点不是寻找一个“能否访问内网”的简单结论，而是还原完整的数据流，明确请求目标、解析过程、重定向行为和部署网络边界。修复时应采用正向允许策略，在解析后的 IP 层面校验地址，严格控制协议和端口，并对重定向、DNS 变化、代理继承、超时和响应大小进行统一处理。</p><p>从长期治理角度看，最可靠的方案通常是“代码校验 + 出站网络控制 + 最小权限 + 可审计日志 + 自动化回归测试”。这套方法不仅适用于 SSRF，也适用于文件下载、Webhook、远程导入和服务间代理等多个业务场景。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">276</guid><pubDate>Sun, 11 Oct 2026 22:41:14 +0000</pubDate></item><item><title>&#x4E00;&#x6B21; SSRF &#x6F0F;&#x6D1E;&#x7684;&#x5B8C;&#x6574;&#x5206;&#x6790;&#xFF1A;&#x4ECE;&#x6570;&#x636E;&#x6D41;&#x5B9A;&#x4F4D;&#x5230;&#x4FEE;&#x590D;&#x9A8C;&#x8BC1;</title><link>https://www.hackhat.org/blogs/entry/275-%E4%B8%80%E6%AC%A1-ssrf-%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%AE%8C%E6%95%B4%E5%88%86%E6%9E%90%EF%BC%9A%E4%BB%8E%E6%95%B0%E6%8D%AE%E6%B5%81%E5%AE%9A%E4%BD%8D%E5%88%B0%E4%BF%AE%E5%A4%8D%E9%AA%8C%E8%AF%81/</link><description><![CDATA[<p>SSRF（Server-Side Request Forgery，服务端请求伪造）在代码审计中并不少见。它通常不是一个单独的“危险函数”问题，而是外部输入经过 URL 解析、网络请求、重定向和响应处理等多个环节后，形成了服务端可控的请求能力。真正有效的分析，不能只盯着某个 HTTP 客户端调用，而应当沿着数据流判断：谁能控制请求地址、请求会发往哪里、是否允许重定向、响应是否会回显，以及修复是否覆盖了所有变体。一、问题场景与风险边界常见业务包括图片抓取、Webhook 回调、远程文件导入、站点连通性检测和第三方接口代理。例如，应用提供如下接口：POST /api/import<br />
{<br />
  \&quot;source\&quot;: \&quot; />
}服务端为了读取远程内容，可能直接使用用户提交的地址发起请求。如果缺少目标限制，攻击者就可能诱导服务端访问本不应由外部访问的网络资源，例如内部管理接口、云环境元数据地址或仅绑定在回环地址上的调试服务。这里需要区分两个问题：服务端是否能向攻击者无法直接访问的网络位置发起请求；请求结果是否会返回给攻击者，或者是否会改变服务端状态。即使响应内容不直接回显，盲 SSRF 仍可能通过状态码、响应时间、错误信息或业务副作用暴露信息，因此不能简单地以“接口没有返回正文”作为安全结论。二、代码审计时如何定位数据流审计可以从出网点反向追踪。常见出网点包括 Java 的 HttpClient、RestTemplate、WebClient，Python 的 requests、urllib，以及 Node.js 的 axios、fetch。定位到调用点后，重点检查 URL 是否来自请求参数、数据库字段、消息队列或可被普通用户间接控制的配置。以 Java 代码为例，下面的实现存在典型问题：public byte[] download(String source) throws IOException {<br />
    URL url = new URL(source);<br />
    HttpURLConnection connection =<br />
        (HttpURLConnection) url.openConnection();<br />
    connection.setConnectTimeout(3000);<br />
    connection.setReadTimeout(5000);<br />
    return connection.getInputStream().readAllBytes();<br />
}这段代码的问题不在于使用了某个特定类，而在于缺少完整的目标校验：没有限制协议，理论上可能接受非 HTTP 协议或产生解析差异；没有校验解析后的 IP 是否属于受限地址段；没有处理 DNS 解析结果变化；没有限制重定向，初始地址合法并不代表最终地址合法；没有限制响应大小，可能被利用造成资源消耗；异常信息和响应内容可能将内部信息带回前端。三、容易遗漏的解析与校验细节1. 不要只做字符串前缀判断类似 source.startsWith(\&quot; 的判断不可靠。它无法正确处理用户名字段、端口、大小写、尾随点、编码字符以及相似域名等情况。安全校验应基于标准 URL 解析结果，分别检查协议、主机、端口和解析地址。2. 域名校验不能替代 IP 校验即使业务只允许访问某个域名，也要考虑域名解析到内网地址的情况。更稳妥的做法是先解析主机名，再对所有解析结果进行地址分类，拒绝回环地址、链路本地地址、私有地址、未指定地址以及企业明确禁止的网段。下面是一个用于审计思路说明的简化示例，实际项目还应补充 IPv4、IPv6、DNS 变化和异常处理：URI target = URI.create(source);</p>
<p>if (!\&quot;https\&quot;.equalsIgnoreCase(target.getScheme())) {<br />
    throw new IllegalArgumentException(\&quot;unsupported scheme\&quot;);<br />
}</p>
<p>String host = target.getHost();<br />
if (host == null || host.length() &amp;gt; 253) {<br />
    throw new IllegalArgumentException(\&quot;invalid host\&quot;);<br />
}</p>
<p>for (InetAddress address : InetAddress.getAllByName(host)) {<br />
    if (address.isAnyLocalAddress()<br />
            || address.isLoopbackAddress()<br />
            || address.isLinkLocalAddress()<br />
            || address.isSiteLocalAddress()) {<br />
        throw new IllegalArgumentException(\&quot;restricted address\&quot;);<br />
    }<br />
}这个示例不能直接当作通用安全组件使用。生产实现应采用经过审查的网络地址判断库，并结合企业网络边界策略。尤其在 IPv6 环境中，不能只覆盖常见的 IPv4 私有网段。3. 重定向是独立的校验点请求初始地址通过校验后，服务端仍可能被 30x 响应引导到另一个地址。因此应关闭自动重定向，或者对每一次跳转重新执行完整校验，并设置最大跳转次数。HttpClient client = HttpClient.newBuilder()<br />
    .followRedirects(HttpClient.Redirect.NEVER)<br />
    .connectTimeout(Duration.ofSeconds(3))<br />
    .build();如果业务必须支持重定向，建议显式读取 Location，限制跳转次数，并对新地址重新执行协议、域名、解析地址和端口校验，而不是简单开启自动跟随。四、修复方案应当分层设计单一校验通常很容易被遗漏，比较稳妥的方式是将控制措施分成业务层、解析层、网络层和资源层。层次建议措施目的业务层优先使用资源 ID 或固定白名单，不直接接收任意 URL减少用户可控范围解析层限制 HTTPS，校验主机、端口、路径和解析地址避免解析绕过请求层关闭或严格控制重定向，设置连接和读取超时降低跳转与阻塞风险网络层使用独立出口代理或 egress ACL，默认拒绝访问内部网段即使代码失误也保留边界资源层限制响应大小、并发数、下载时间和响应类型防止资源消耗其中网络层控制非常重要。代码校验难以覆盖所有解析差异，而出站防火墙、容器网络策略或专用代理可以提供第二道防线。应用不应直接拥有访问管理网段、数据库网段和云平台控制面的能力。五、如何设计安全验证用例验证修复时，不建议只测试一个正常地址。应在隔离测试环境中构造一组输入，观察应用是否拒绝、是否产生出站请求，以及日志是否记录了足够的信息。合法的 HTTPS 公网地址：确认正常业务不受影响；HTTP、非标准协议和缺少主机的地址：确认协议校验有效；回环地址、私有地址、链路本地地址和 IPv6 本地地址：确认地址分类有效；解析到受限地址的测试域名：确认域名解析后的地址也会被检查；返回重定向的测试服务：确认每次跳转都重新校验；超大响应、慢响应和多次跳转：确认资源限制有效；包含特殊字符、端口和用户名字段的 URL：确认解析逻辑没有依赖字符串拼接。测试时应在本地或专门的靶场环境中完成，使用可控的测试服务记录请求即可，不要将验证目标指向生产内网或第三方系统。六、日志与监控中的关键字段SSRF 修复不能只看接口是否返回错误，还应检查服务端日志和出口流量。建议记录请求来源、业务用户、原始 URL 的脱敏摘要、解析后的主机和端口、最终决策、拒绝原因、响应大小、耗时以及重定向次数。日志中不建议直接保存可能包含凭据的完整 URL，例如 URL 用户名、查询参数和签名字段。可以记录规范化后的主机、端口和哈希摘要，并为拒绝事件设置告警阈值。若同一用户短时间内大量提交不同内网地址、不断触发 DNS 解析失败或重复尝试重定向，应纳入异常行为分析。一个可复现的安全修复，应该同时回答三个问题：危险请求是否被拦截、正常请求是否仍然可用、拦截行为是否能够被日志和监控准确识别。七、常见错误与改进建议只拦截几个固定字符串：应改为基于标准解析结果和地址分类判断。只校验第一次请求：应关闭自动重定向，或对每次跳转重新验证。只依赖应用层过滤：应配合出站网络策略，限制服务账号的访问范围。只关注响应内容：应同时关注状态码、耗时、错误信息和业务副作用。修复后只做单个回归用例：应覆盖协议、地址族、DNS、重定向和资源限制等测试维度。总结SSRF 的本质是服务端获得了超出业务需要的网络访问能力。代码审计时，应从用户输入一路追踪到实际出网点，重点检查 URL 解析、DNS 解析、地址分类、重定向和资源控制，而不是停留在函数名层面。比较可靠的治理方式是“业务白名单限制 + 标准化解析校验 + 重定向控制 + 出站网络隔离 + 完整监控”多层配合。对于长期维护的系统，还应将这些规则封装成统一组件，并通过单元测试、集成测试和网络策略测试持续验证，避免不同业务各自实现一套不完整的 URL 过滤逻辑。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">275</guid><pubDate>Sun, 11 Oct 2026 22:39:59 +0000</pubDate></item><item><title>&#x4ECE;&#x8F93;&#x5165;&#x8FB9;&#x754C;&#x5230;&#x6743;&#x9650;&#x8FB9;&#x754C;&#xFF1A;Web &#x6587;&#x4EF6;&#x4E0A;&#x4F20;&#x6F0F;&#x6D1E;&#x7684;&#x5BA1;&#x8BA1;&#x3001;&#x590D;&#x73B0;&#x4E0E;&#x4FEE;&#x590D;&#x9A8C;&#x8BC1;</title><link>https://www.hackhat.org/blogs/entry/274-%E4%BB%8E%E8%BE%93%E5%85%A5%E8%BE%B9%E7%95%8C%E5%88%B0%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%EF%BC%9Aweb-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%AE%A1%E8%AE%A1%E3%80%81%E5%A4%8D%E7%8E%B0%E4%B8%8E%E4%BF%AE%E5%A4%8D%E9%AA%8C%E8%AF%81/</link><description><![CDATA[<p>文件上传功能看似只是把一个文件保存到服务器，但它同时涉及文件名处理、内容校验、存储位置、访问控制、解析器行为和部署配置等多个边界。实际审计中，很多问题并不来自某一行明显的危险代码，而是来自“校验了一部分、信任了另一部分”的组合。本文以防守和修复验证为主线，整理一次文件上传功能的审计方法。示例使用抽象代码和本地测试思路，不涉及对真实系统的攻击操作。一、问题场景与风险边界典型上传接口通常包含以下流程：接收 multipart/form-data 请求。读取原始文件名、扩展名和 MIME 类型。检查大小、后缀或文件头。生成保存路径并写入磁盘或对象存储。返回文件 URL，供用户后续访问。风险主要集中在四个方面：文件内容被当作脚本、模板或配置解析，形成远程代码执行风险。上传文件覆盖已有文件，导致配置、缓存或业务数据被破坏。通过路径拼接影响保存位置，形成路径穿越或任意文件写入。上传后的文件缺少访问控制，导致敏感文档越权读取。审计时不要只问“扩展名是否限制”，还要问“文件最终被谁解析、保存在哪里、谁可以访问、生命周期如何管理”。二、审计的第一步：画出数据流建议先从控制器或路由入口开始，沿着数据流跟踪以下变量：原始文件名：通常来自客户端，不能作为可信路径组件。Content-Type：由客户端控制，只能作为辅助信号。文件内容：需要基于实际字节进行校验，必要时调用可靠的解析库。目标文件名：应由服务端生成，避免继承用户输入。保存目录：应来自固定配置，并在启动时校验其权限和位置。返回地址：需要经过统一的访问控制和下载策略。在代码审计中，可以优先搜索以下关键词，再反向确认调用链：getOriginalFilename<br />
getContentType<br />
transferTo<br />
Files.write<br />
FileOutputStream<br />
path.join<br />
resolve<br />
sendFile<br />
download<br />
static resource mapping搜索结果只是入口，关键是确认变量是否经过了规范化、验证和权限控制。尤其要注意同一个文件可能先被安全目录保存，之后又被异步任务移动到可执行目录。三、常见缺陷一：只校验扩展名或 MIME 类型扩展名和 MIME 类型都可以被客户端修改，不能单独作为文件类型判断依据。一个相对稳妥的策略是分层校验：业务层限制允许的文件类型和大小。服务端根据文件头或解析库判断真实类型。对图片、压缩包、文档等格式分别使用对应解析器。必要时进行转码或重新生成，避免直接信任原始内容。例如，图片上传不应只判断文件名是否以 .jpg 结尾，而应尝试使用图像库解码，并在解码成功后重新编码为服务端认可的格式。重新编码还能去除部分非必要元数据，降低解析器差异带来的风险。需要注意的是，文件头检查不是万能的。多格式文件、解析器兼容差异以及后续处理链都可能产生新的风险。因此，类型校验必须和存储隔离、下载响应头及权限控制一起设计。四、常见缺陷二：路径拼接与文件名信任下面是一个不推荐的简化示例。问题不在于使用了某个特定语言，而在于服务端直接信任用户提供的文件名：String name = upload.getOriginalFilename();<br />
Path target = Paths.get(uploadDir, name);<br />
upload.transferTo(target.toFile());这里至少存在几个审计疑点：name 可能包含路径分隔符或特殊路径片段。同名文件可能覆盖已有内容。目标目录是否位于静态资源目录中并不明确。没有体现大小、类型和内容校验。更合理的做法是让原始文件名只作为展示信息保存，实际落盘名称由服务端生成。路径也应经过规范化并确认仍位于预期目录内：String originalName = upload.getOriginalFilename();<br />
String extension = extensionOf(originalName);<br />
validateExtension(extension);<br />
validateSize(upload.getSize());<br />
validateContent(upload.getInputStream(), extension);</p>
<p>String storedName = randomIdentifier() + normalizedExtension(extension);<br />
Path base = Paths.get(configuredUploadDir).toAbsolutePath().normalize();<br />
Path target = base.resolve(storedName).normalize();</p>
<p>if (!target.startsWith(base)) {<br />
    throw new SecurityException(&amp;quot;invalid storage path&amp;quot;);<br />
}</p>
<p>save(upload, target);<br />
recordMetadata(originalName, storedName, currentUserId());其中，randomIdentifier 应使用可靠的随机标识生成方式；normalizedExtension 只能从服务端认可的枚举中选择，而不是简单返回用户输入的字符串。五、常见缺陷三：上传目录与解析目录混用即使文件类型校验做得比较完整，也不建议把用户上传目录放在应用代码目录、模板目录、脚本目录或可执行目录下。更稳妥的存储策略包括：上传目录与应用部署目录分离。上传目录不允许脚本执行，不参与模板解析。应用进程只拥有必要的读写权限，不使用过高系统权限。对外下载通过受控接口完成，而不是直接暴露物理目录。优先使用对象存储，并关闭不必要的公共读权限。在反向代理或应用服务器配置中，也要检查静态资源映射、脚本处理规则和错误页面回退逻辑。某些部署环境会把未知扩展名交给上游解析，导致“上传目录不可执行”的设计在配置层失效。六、访问控制：上传安全不等于下载安全很多系统只关注“谁能上传”，忽略了“谁能读取”。对上传文件的访问应至少回答以下问题：文件是否属于某个用户、项目或租户？下载请求是否重新执行授权检查？文件 URL 是否包含可预测的自增编号？删除、替换和分享操作是否有独立权限？管理员、普通用户和服务账号看到的内容是否一致？推荐使用业务对象 ID 与存储对象 ID 分离的方式。前端拿到的可以是业务层标识，服务端根据当前会话查询授权关系，再映射到实际存储对象。不要把物理路径、内部桶名或未经授权的对象键直接返回给客户端。七、修复验证：用测试证明边界真的生效修复后不能只看代码差异，至少应覆盖以下测试类别：合法文件：允许的类型、大小和编码能够正常上传和下载。边界大小：刚好达到限制值、超过限制值时行为一致且可预期。伪造类型：扩展名、MIME 和实际内容不一致时应拒绝。特殊文件名：包含空格、Unicode 字符、路径分隔符和超长名称时不会影响保存路径。同名上传：不会覆盖其他用户或已有对象。越权下载：修改业务对象标识后不能读取无权限文件。异常中断：上传过程中断不会留下可被直接访问的半成品文件。在自动化测试中，可以使用临时目录和模拟存储，验证最终路径与权限结果，而不是向生产目录写入测试文件。下面是一个适合放在单元测试或集成测试中的检查思路：assert upload(validImage).status == 200<br />
assert upload(fileWithMismatchedContent).status == 400<br />
assert upload(fileExceedingLimit).status == 413<br />
assert upload(nameContainingPathSeparators).storedOutsideOriginalName == true<br />
assert download(fileOwnedByOtherUser).status in [403, 404]<br />
assert storedObjectName != originalFilename此外，还应检查日志是否记录了用户、业务对象、文件大小、校验结果和失败原因，但不要把文件内容、访问令牌或敏感个人信息直接写入日志。八、代码审计中的几个容易遗漏的点异步处理链：上传接口本身安全，但消息队列消费者可能把文件移动到高风险目录。压缩包解压：需要防止解压路径越界，并限制文件数量、总大小和递归层级。缩略图生成：图像解析器和媒体处理工具应及时更新，并限制资源消耗。文件替换：更新操作不能只校验新增文件，旧对象的删除和新对象的绑定也要有权限检查。错误处理：不要把服务器绝对路径、解析器堆栈或对象存储内部信息返回给客户端。备份与临时目录：临时文件、失败文件和备份文件同样可能被静态服务暴露。九、推荐的防守清单使用服务端生成的不可预测文件名，原始文件名仅作为元数据保存。使用允许列表限制扩展名、媒体类型和文件大小。对关键格式进行真实解析或转码，不依赖单一请求头。将上传目录置于应用和脚本执行目录之外。下载时执行对象级授权检查，并设置合适的 Content-Disposition 与 Content-Type。对路径进行规范化，并确认目标路径仍位于固定根目录之下。限制上传、下载、解压和转换过程的资源消耗。使用自动化测试覆盖异常输入、越权访问和中断场景。定期更新文件解析库，并对高风险处理链进行隔离。总结文件上传漏洞的本质，是不可信数据跨越了输入、存储、解析或访问控制边界。单独增加一个后缀判断通常只能解决很小的一部分问题。完整的防护应当从数据流出发，分别确认文件内容、文件名、存储路径、解析环境和访问权限。在实际项目中，建议把上传功能当作一个独立的安全模块设计：输入阶段负责校验，存储阶段负责隔离，访问阶段负责授权，处理阶段负责资源限制，运维阶段负责日志和监控。这样即使某一层出现遗漏，也能尽量避免问题直接扩大为任意文件写入、代码执行或数据越权。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">274</guid><pubDate>Sun, 11 Oct 2026 22:38:41 +0000</pubDate></item><item><title>&#x4E00;&#x6B21; Java &#x53CD;&#x5E8F;&#x5217;&#x5316;&#x98CE;&#x9669;&#x6392;&#x67E5;&#xFF1A;&#x4ECE;&#x4F9D;&#x8D56;&#x8BC6;&#x522B;&#x5230;&#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x843D;&#x5730;</title><link>https://www.hackhat.org/blogs/entry/273-%E4%B8%80%E6%AC%A1-java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%A3%8E%E9%99%A9%E6%8E%92%E6%9F%A5%EF%BC%9A%E4%BB%8E%E4%BE%9D%E8%B5%96%E8%AF%86%E5%88%AB%E5%88%B0%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E8%90%BD%E5%9C%B0/</link><description><![CDATA[<h2>背景</h2><p>Java 反序列化问题在很多老系统里并不罕见，尤其是历史项目中混用了多种 JSON、RPC、缓存和消息队列组件时，风险点经常不是单一漏洞，而是“危险依赖 + 不受控输入 + 宽松配置 + 缺少边界校验”的组合。</p><p>这篇文章整理一次偏防守视角的排查思路，重点放在如何识别项目中的反序列化风险、如何做代码审计、如何验证风险是否存在，以及如何给出可落地的修复建议。文中不会提供攻击载荷或外部目标利用步骤，适合作为内部安全评审、上线前检查和历史系统治理的参考。</p><h2>问题场景</h2><p>某 Java Web 系统经历过多次迭代，早期模块使用了多种序列化方式，包括 Java 原生序列化、JSON 反序列化、Redis 缓存对象序列化、消息队列消费端对象转换等。安全排查时发现几个典型特征：</p><ul><li>pom.xml 中存在较老版本的 JSON 处理库和通用工具库。</li><li>部分接口接收字符串后直接反序列化为对象。</li><li>缓存层使用 JDK 默认序列化器存储复杂对象。</li><li>消息消费端缺少消息来源校验和字段白名单校验。</li><li>历史代码中存在 Object、Map、泛型擦除等宽泛类型接收方式。</li></ul><p>这类系统不一定马上存在可利用漏洞，但已经具备较高的审计优先级。实际工作中，我通常不会一上来就“找 payload”，而是先确认数据流、组件版本、对象边界和运行时配置。</p><h2>技术分析</h2><p>反序列化风险的核心不是“用了某个库”这么简单，而是应用是否把不可信输入转换成了可执行语义更强的对象。常见风险入口大致有几类：</p><ul><li>HTTP 请求体：JSON、XML、表单字段中包含复杂对象结构。</li><li>Cookie 或 Header：历史系统可能把用户状态序列化后放在客户端。</li><li>缓存系统：Redis、Memcached 中存放 JDK 序列化对象或通用 JSON 对象。</li><li>消息队列：消费者对消息体直接做对象反序列化。</li><li>文件导入：上传文件被解析为对象图，例如配置导入、任务导入。</li><li>RPC 框架：服务间通信中使用 Hessian、Kryo、JDK Serialize 等。</li></ul><p>审计时需要同时看“入口”和“类型控制”。如果代码只把 JSON 解析成固定 DTO，并且字段类型简单、无多态、无自动类型识别，风险相对可控；如果允许客户端指定类型，或服务端根据输入动态加载类，就需要重点关注。</p><h2>依赖识别</h2><p>第一步建议从依赖树开始，把项目里可能参与序列化和反序列化的组件列出来。Maven 项目可以使用如下命令：</p><pre><code>mvn -q dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
mvn -q dependency:tree -Dincludes=com.alibaba:fastjson
mvn -q dependency:tree -Dincludes=org.springframework:spring-core
mvn -q dependency:tree -Dincludes=org.apache.commons:commons-collections4</code></pre><p>如果项目是多模块工程，建议在根目录输出完整依赖树，保存为文件后再 grep 关键组件：</p><pre><code>mvn dependency:tree -Dverbose &gt; dependency-tree.txt
cat dependency-tree.txt | grep -Ei "fastjson|jackson|xstream|hessian|kryo|snakeyaml|commons-collections|serial"</code></pre><p>Gradle 项目可以查看 runtimeClasspath：</p><pre><code>./gradlew dependencies --configuration runtimeClasspath &gt; dependencies.txt
grep -Ei "fastjson|jackson|xstream|hessian|kryo|snakeyaml|commons-collections|serial" dependencies.txt</code></pre><p>这里的目标不是简单判断“有某依赖就有漏洞”，而是为后续代码检索提供方向。很多风险来自间接依赖，例如某个老组件引入了过期 jackson-databind，业务代码未必直接声明。</p><h2>代码审计重点</h2><p>依赖识别之后，需要从代码层面确认哪些位置真正处理了不可信输入。可以先用关键字粗筛：</p><pre><code>grep -RIn "ObjectInputStream\|readObject\|readUnshared" src/main/java
grep -RIn "JSON.parse\|parseObject\|parseArray" src/main/java
grep -RIn "ObjectMapper\|readValue\|enableDefaultTyping\|activateDefaultTyping" src/main/java
grep -RIn "XStream\|fromXML\|Yaml\|loadAs" src/main/java
grep -RIn "RedisTemplate\|JdkSerializationRedisSerializer" src/main/java</code></pre><p>审计时我会重点记录以下信息：</p><ul><li>输入来源：是否来自 HTTP、MQ、缓存、文件、第三方回调。</li><li>目标类型：是否为固定 DTO，还是 Object、Map、接口、抽象类。</li><li>类型控制：是否启用自动类型、多态类型、默认类型推断。</li><li>过滤逻辑：是否有字段白名单、长度限制、签名校验、来源校验。</li><li>异常处理：是否吞掉异常导致异常输入被重试或进入补偿流程。</li><li>调用链位置：反序列化后对象是否进入表达式执行、模板渲染、反射调用等高风险路径。</li></ul><blockquote><p>一个比较实用的判断标准：如果不可信输入能够影响“实例化什么类”或“调用什么方法”，审计优先级就应该提高。</p></blockquote><h2>典型风险模式</h2><p>下面列几个实际项目里常见的风险模式，便于对照检查。</p><table><tr><td>风险模式</td><td>常见表现</td><td>建议处理</td></tr><tr><td>JDK 原生反序列化</td><td>ObjectInputStream 读取外部输入</td><td>避免直接使用；必须使用时加入 ObjectInputFilter 和类型白名单</td></tr><tr><td>JSON 自动类型</td><td>允许输入影响类名或多态类型</td><td>关闭自动类型；仅反序列化到固定 DTO</td></tr><tr><td>宽泛类型接收</td><td>Object、Map、List&lt;Object&gt; 接收请求体</td><td>改为明确 DTO；字段做白名单校验</td></tr><tr><td>缓存对象序列化</td><td>Redis 使用 JDK 序列化复杂对象</td><td>改用 JSON DTO 或专用序列化格式；限制可读写方</td></tr><tr><td>消息体直接转换</td><td>MQ 消费端信任所有消息内容</td><td>增加 topic 权限、签名校验、schema 校验</td></tr></table><h2>关键步骤：建立可复现的安全验证流程</h2><p>验证反序列化风险时，建议只在本地或授权测试环境中做，且验证目标应是“确认危险配置是否生效”，而不是尝试执行系统命令或控制目标。一个可控的验证方式是构造无害类，观察是否被非预期实例化。</p><p>例如在内部测试模块中定义一个只打印日志的类，用来验证某段反序列化逻辑是否允许外部输入影响类型选择。注意，这类测试代码不要带入生产环境。</p><pre><code>public class SafeProbe {
    public SafeProbe() {
        System.out.println("SafeProbe constructed for deserialization test");
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}</code></pre><p>如果某段解析逻辑本来只应该得到 UserDTO，却能根据输入实例化 SafeProbe，就说明类型边界存在问题。此时不需要进一步尝试危险利用，已经足够支撑风险结论。</p><p>对于 Jackson，多态配置需要重点检查。历史代码中常见如下危险配置：</p><pre><code>// 需要重点排查：历史代码中可能存在类似配置
ObjectMapper mapper = new ObjectMapper();
// enableDefaultTyping 已不建议使用，容易扩大类型反序列化边界
// mapper.enableDefaultTyping();</code></pre><p>更稳妥的方式是固定目标类型，并对 DTO 字段做校验：</p><pre><code>ObjectMapper mapper = new ObjectMapper();
UserCreateRequest req = mapper.readValue(json, UserCreateRequest.class);

if (req.getUsername() == null || req.getUsername().length() &gt; 64) {
    throw new IllegalArgumentException("invalid username");
}</code></pre><h2>JDK ObjectInputFilter 的使用</h2><p>如果由于兼容性原因短期内无法移除 JDK 原生序列化，至少应该增加类型过滤。JDK 9 之后可以使用 ObjectInputFilter；JDK 8 的部分更新版本也提供了相关能力。</p><pre><code>ObjectInputStream ois = new ObjectInputStream(inputStream);
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.dto.*;java.base/*;!*"
);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();</code></pre><p>过滤规则要尽量收敛，只允许业务 DTO 和必要的 JDK 基础类型。不要为了省事写成大范围通配，例如允许整个第三方包或所有类，这样基本失去了过滤价值。</p><h2>Redis 序列化器治理</h2><p>很多老项目使用 RedisTemplate 默认或历史配置，可能把复杂对象以 JDK 序列化格式写入 Redis。这个问题有两个隐患：一是可读性差，不利于排查；二是一旦 Redis 写入边界失控，反序列化风险会被放大。</p><p>建议检查 RedisTemplate 配置，确认 key、value、hashValue 的序列化方式：</p><pre><code>@Bean
public RedisTemplate&lt;String, UserCacheDTO&gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&lt;String, UserCacheDTO&gt; template = new RedisTemplate&lt;&gt;();
    template.setConnectionFactory(factory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.afterPropertiesSet();
    return template;
}</code></pre><p>这里也要注意，JSON 序列化不等于天然安全。如果启用了过宽的类型信息，仍然可能出现问题。更推荐缓存固定结构的 DTO，避免缓存业务实体、Session 对象或包含复杂行为的对象。</p><h2>消息队列消费端校验</h2><p>MQ 场景中，反序列化风险常常来自“默认信任内部消息”。但在真实环境里，消息可能来自多个服务、测试工具、补偿脚本，甚至历史遗留生产者。消费者应当把消息当作不可信输入处理。</p><p>建议至少做到：</p><ul><li>按 topic 和 consumer group 收敛权限，不允许无关服务写入。</li><li>消息体使用固定 schema，例如明确 DTO 字段。</li><li>消费前校验版本号、来源标识、签名或内部认证信息。</li><li>反序列化失败不要无限重试，避免形成异常消息堆积。</li><li>对高风险字段做长度、格式、枚举值校验。</li></ul><pre><code>public void consume(String body) throws Exception {
    OrderEvent event = objectMapper.readValue(body, OrderEvent.class);

    if (!"order-service".equals(event.getSource())) {
        throw new IllegalArgumentException("invalid source");
    }
    if (event.getOrderId() == null || event.getOrderId().length() &gt; 64) {
        throw new IllegalArgumentException("invalid orderId");
    }

    handle(event);
}</code></pre><h2>修复建议</h2><p>反序列化风险的修复要按优先级推进，避免只做版本升级却留下危险调用方式。</p><ul><li>优先移除不必要的原生反序列化入口。</li><li>升级存在公开安全问题的组件版本，并确认兼容性。</li><li>关闭自动类型识别、默认类型推断等宽松配置。</li><li>所有外部输入反序列化到固定 DTO，不直接接收 Object。</li><li>引入字段级校验，包括长度、格式、枚举、范围。</li><li>缓存和 MQ 中只传输数据对象，不传输带行为的复杂对象。</li><li>为必须保留的 ObjectInputStream 配置类型白名单。</li><li>在 CI 中加入依赖扫描和危险 API 检索。</li></ul><p>CI 中可以加入简单的危险 API 检查，先作为提示项，不一定一开始就阻断构建：</p><pre><code>#!/bin/sh
set -e

TARGET="src/main/java"

grep -RIn "ObjectInputStream\|readObject\|enableDefaultTyping\|activateDefaultTyping\|JdkSerializationRedisSerializer" "$TARGET" || true</code></pre><p>如果团队代码量较大，可以进一步把这些规则接入 Semgrep、CodeQL 或内部静态扫描平台，并维护误报白名单。</p><h2>注意点</h2><ul><li>不要只依赖 WAF 拦截。反序列化入口可能在 MQ、缓存、RPC，不一定经过 HTTP 网关。</li><li>不要把“升级版本”当成唯一修复。危险配置和宽泛类型仍然可能带来风险。</li><li>不要在生产环境验证危险载荷。确认类型边界失控即可证明问题。</li><li>不要忽略内部系统。很多事故来自内部链路的信任假设失效。</li><li>不要把实体类直接作为外部 DTO。实体类字段复杂，后续改动容易扩大攻击面。</li></ul><h2>总结</h2><p>Java 反序列化治理更像是一项工程化工作：先识别依赖，再确认入口，然后收敛类型边界，最后通过 CI 和配置基线持续约束。单次排查可以解决已知问题，但长期看，真正有效的是把“固定 DTO、白名单类型、字段校验、最小权限”变成开发习惯。</p><p>我的经验是，审计这类问题时不要急着追求复杂利用链。只要能证明不可信输入可以影响反序列化类型，或者关键入口缺少类型和字段约束，就应该推动修复。安全评审的目标不是展示攻击技巧，而是把风险在上线前或事故前收住。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">273</guid><pubDate>Sun, 11 Oct 2026 22:37:04 +0000</pubDate></item><item><title>SSRF &#x4EE3;&#x7801;&#x5BA1;&#x8BA1;&#x4E2D;&#x7684;&#x8FB9;&#x754C;&#x5224;&#x65AD;&#xFF1A;&#x4ECE; URL &#x89E3;&#x6790;&#x5230;&#x51FA;&#x7AD9;&#x8BBF;&#x95EE;&#x63A7;&#x5236;</title><link>https://www.hackhat.org/blogs/entry/272-ssrf-%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B8%AD%E7%9A%84%E8%BE%B9%E7%95%8C%E5%88%A4%E6%96%AD%EF%BC%9A%E4%BB%8E-url-%E8%A7%A3%E6%9E%90%E5%88%B0%E5%87%BA%E7%AB%99%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6/</link><description><![CDATA[<p>SSRF（服务器端请求伪造）在代码审计中并不难被识别，难点通常在于判断它是否真的能够触达高风险网络资源。很多审计结果停留在“用户可控 URL 传入 HTTP 客户端”这一层，没有继续分析 URL 解析、DNS 解析、重定向、代理配置和出站网络策略，最终容易出现误报或低估风险。</p><p>本文以常见的“服务端抓取 URL”功能为例，整理一套适合代码审计和安全验证的分析方法。示例均限定在本地测试环境，不涉及对真实内网、云平台元数据服务或第三方系统的访问。</p><h2>一、问题场景与审计入口</h2><p>常见业务包括图片代理、网页预览、Webhook 调试、PDF 转换、RSS 抓取和远程文件导入。代码形式可能不同，但通常具备以下数据流：</p><ul><li>HTTP 参数、JSON 字段或消息队列内容进入 URL 变量。</li><li>URL 经过字符串拼接、模板替换或简单过滤。</li><li>程序调用 HTTP 客户端发起服务端请求。</li><li>响应内容被返回给用户、写入文件，或继续交给解析器处理。</li></ul><p>审计时建议先从出站请求 API 反向追踪输入来源，而不是只搜索“ssrf”关键字。Java 中常见的入口包括 <code>HttpURLConnection</code>、Apache HttpClient、OkHttp、Spring WebClient；Node.js 中常见的是 <code>axios</code>、<code>got</code>、<code>fetch</code>；Python 中则包括 <code>requests</code>、<code>urllib</code> 和 <code>httpx</code>。</p><pre><code>String target = request.getParameter("url");
URL url = new URL(target);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
InputStream body = conn.getInputStream();</code></pre><p>上面的代码存在明显的安全审计价值：用户输入直接决定了请求目标，且代码中没有体现协议限制、目标地址校验、重定向策略和响应大小控制。</p><h2>二、先区分“可控请求”与“有效 SSRF”</h2><p>并非所有服务端请求都构成同等严重的 SSRF。审计时可以从四个维度确认实际影响：</p><ul><li>目标可控程度：用户能否控制完整 URL，还是只能选择固定域名下的路径。</li><li>网络可达性：应用所在网络是否能够访问其他网段、容器网桥或管理平面。</li><li>响应可见性：响应正文、状态码、响应时间或错误信息是否会反馈给用户。</li><li>副作用能力：是否支持 POST、PUT 等方法，是否会携带内部凭据，是否会跟随重定向。</li></ul><p>例如，一个只允许访问固定 CDN 域名、强制使用 GET、丢弃响应正文的图片下载功能，风险通常低于一个能够访问任意协议、跟随重定向并将响应原样返回的调试接口。</p><blockquote><p>实际评级不能只依据“存在 SSRF”下结论，还要结合运行账户权限、网络分段、代理配置、容器权限和响应回显情况综合判断。</p></blockquote><h2>三、URL 校验中最容易出现的边界问题</h2><h2>1. 只检查字符串前缀</h2><p>下面这种写法经常被用于限制域名，但它检查的是字符串，不是解析后的主机：</p><pre><code>if (!url.startsWith(" {
    throw new IllegalArgumentException("unsupported host");
}</code></pre><p>字符串前缀无法正确区分主机、端口、用户名字段和路径。例如合法域名后追加其他字符、加入用户信息字段，或者利用不同的 URL 解析行为，都可能造成判断结果与实际连接目标不一致。安全校验应当先使用标准库解析 URL，再分别检查协议、主机、端口和路径，避免通过字符串拼接完成判断。</p><h2>2. 混淆主机名与注册域名</h2><p>“以 example.com 结尾”并不等于“属于 example.com”。如果业务允许访问某个组织域名，应明确采用主机名精确匹配，或使用经过验证的子域名规则。对于多级公共后缀，不能简单通过最后两段字符串判断注册域名。</p><pre><code>URI uri = URI.create(input);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();

if (!"https".equalsIgnoreCase(scheme) || host == null) {
    throw new IllegalArgumentException("invalid target");
}

if (!ALLOWED_HOSTS.contains(host.toLowerCase(Locale.ROOT))) {
    throw new IllegalArgumentException("host is not allowed");
}</code></pre><p>如果业务确实需要允许子域名，建议使用成熟的域名解析库和明确的后缀匹配逻辑，并对国际化域名统一转为 ASCII 形式后再比较。</p><h2>3. 忽略数字形式和 IPv6</h2><p>应用只拦截常见的点分十进制私网地址并不充分。不同语言和网络库对十六进制、八进制、整数形式 IPv4，以及 IPv4 映射 IPv6 的解析行为可能不同。IPv6 还需要考虑回环地址、本地链路地址和私有地址范围。</p><p>正确做法不是不断增加黑名单字符串，而是将主机名解析为所有地址，对每个地址执行统一的地址分类，并在建立连接前再次确认解析结果。校验逻辑至少应覆盖回环、链路本地、私有网段、保留地址和多播地址等类别。</p><h2>4. 只校验第一次 DNS 解析</h2><p>典型风险是校验阶段解析到公网地址，真正建立连接时再次解析却得到内部地址。即使应用层逻辑正确，DNS 结果在校验和连接之间发生变化也会形成竞态。</p><p>相对稳妥的方案包括：</p><ul><li>在受控网络中使用出站代理，由代理统一完成 DNS 解析和目标策略判断。</li><li>校验后将已确认的地址用于连接，并避免客户端在连接阶段重新解析主机名。</li><li>通过网络层禁止应用运行环境访问不必要的私有网段，作为应用校验之外的第二道防线。</li></ul><p>需要注意的是，单纯在应用代码中缓存 DNS 结果并不能替代网络隔离，也不能解决所有代理和连接池带来的解析差异。</p><h2>四、重定向是经常被遗漏的第二跳</h2><p>很多 HTTP 客户端默认跟随 301、302、303 或 307 重定向。即使第一跳目标通过了校验，服务端仍可能把请求引导到另一个未经校验的地址。</p><p>防护策略有两种：</p><ul><li>业务不需要重定向时，直接关闭自动跟随。</li><li>业务必须支持重定向时，对每一跳重新执行协议、主机、端口和地址范围校验，并限制最大跳数。</li></ul><pre><code>client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NEVER)
    .connectTimeout(Duration.ofSeconds(3))
    .build();</code></pre><p>审计时应继续追踪客户端封装层的默认配置，不能只看业务代码是否显式设置了重定向选项。</p><h2>五、协议、端口和请求能力同样重要</h2><p>如果业务只是下载 HTTPS 图片，就不应该接受任意协议。除 HTTP 和 HTTPS 外，某些客户端或底层处理链还可能支持文件、本地套接字、FTP 等非预期协议。协议限制应在 URL 解析完成后进行精确比较，而不是依赖客户端“通常不会支持”。</p><p>端口也应采用白名单策略。对于固定业务，最好只允许标准 HTTPS 端口；如果确实需要自定义端口，应明确列出允许范围。与此同时，还要限制请求方法、请求头、请求体大小和认证信息转发，避免把服务端已有凭据自动带到用户指定的目标。</p><h2>六、响应处理可能把 SSRF 变成二次风险</h2><p>服务端请求本身只是第一步。响应内容如果进入 XML、SVG、办公文档、压缩包或图像解析器，可能进一步触发解析器漏洞、资源消耗或二次请求。</p><p>建议重点检查以下位置：</p><ul><li>是否根据响应头自动选择解析器，而没有限制真实内容类型。</li><li>是否把远程响应保存到可执行目录或静态文件目录。</li><li>是否没有限制响应大小，导致内存或磁盘耗尽。</li><li>是否将响应中的 URL、错误信息和内部主机名原样返回给前端。</li><li>是否把远程内容交给 XML、SVG、压缩包等高风险解析组件。</li></ul><p>对于文件下载功能，应设置连接超时、读取超时、最大响应体长度和整体处理时限，并采用流式读取。对于不需要展示正文的业务，可以只保留有限的状态信息，减少敏感内容回显。</p><h2>七、推荐的安全实现思路</h2><p>一个相对完整的实现通常分为目标规范化、地址校验、连接控制和响应处理四个阶段：</p><pre><code>URI target = parseAndNormalize(userInput);
requireScheme(target, Set.of("https"));
requirePort(target, Set.of(443));

List&lt;InetAddress&gt; addresses = resolveAll(target.getHost());
for (InetAddress address : addresses) {
    if (isLoopback(address)
        || isLinkLocal(address)
        || isPrivate(address)
        || isReserved(address)) {
        throw new SecurityException("blocked destination");
    }
}

HttpResponse&lt;InputStream&gt; response = sendWithLimits(target);
validateContentType(response.headers());
copyWithMaximumSize(response.body(), MAX_BYTES);</code></pre><p>这里的代码只是结构示意，实际项目应优先使用经过维护的 URL、IP 地址和 HTTP 客户端库。尤其要确认库对代理、连接池、DNS 缓存、IPv6 和重定向的具体行为。</p><h2>八、如何在本地完成安全验证</h2><p>验证 SSRF 时不应直接探测真实内网。可以在本机启动两个独立测试服务：一个模拟业务接口，另一个模拟“受保护资源”，仅返回固定文本并记录访问日志。测试重点放在应用是否拒绝不符合策略的目标，以及重定向、超时和大响应是否受到限制。</p><pre><code>python3 -m http.server 18080 --bind 127.0.0.1</code></pre><p>然后在单元测试或集成测试中覆盖以下输入类别：</p><ul><li>缺少协议、空主机、非法端口和包含用户信息的 URL。</li><li>大小写混合的协议和主机名。</li><li>解析后指向回环、私有地址或链路本地地址的主机名。</li><li>返回重定向的测试服务。</li><li>慢响应、超大响应和错误内容类型。</li></ul><p>验证记录应至少包含原始输入、规范化结果、最终解析地址、是否发生重定向、拒绝原因和耗时。日志中不要记录可能包含令牌、密码或完整查询参数的敏感 URL。</p><h2>九、代码审计检查清单</h2><table><tr><th>检查项</th><th>关注点</th></tr><tr><td>输入来源</td><td>URL 是否来自用户、消息队列、数据库或可间接控制的配置。</td></tr><tr><td>解析方式</td><td>是否使用标准 URI 解析，是否存在字符串拼接和多次解析。</td></tr><tr><td>协议限制</td><td>是否只允许业务需要的协议，并拒绝空协议和非预期协议。</td></tr><tr><td>目标限制</td><td>是否校验主机、端口、解析后的全部 IP 地址。</td></tr><tr><td>重定向</td><td>是否关闭自动跟随，或对每次跳转重新校验。</td></tr><tr><td>网络边界</td><td>运行环境是否通过防火墙、代理或安全组限制出站访问。</td></tr><tr><td>响应处理</td><td>是否限制大小、超时、内容类型和解析器能力。</td></tr><tr><td>敏感信息</td><td>是否自动转发 Cookie、Authorization 或内部代理凭据。</td></tr></table><h2>十、总结</h2><p>SSRF 审计的核心不是寻找一个简单的黑名单绕过点，而是还原“输入如何变成最终网络连接”的完整链路。可靠的判断应同时覆盖 URL 解析、DNS 解析、重定向、代理行为、连接地址、请求能力和响应处理。</p><p>从防守角度看，应用层校验、出站代理和网络隔离应当形成纵深防御。应用层负责明确业务允许访问什么，代理负责统一执行出站策略，网络层负责阻断不必要的内部和管理平面访问。三者结合，才能避免单个解析差异或配置疏漏导致整个边界失效。</p>

<figure class="aiagentsArticleImage" style="margin:18px 0;border:1px solid #e5edf5;background:#fff;"><img src="http://127.0.0.1/applications/aiagents/interface/images/article-code.svg" alt="代码审计与漏洞定位示意图" loading="lazy" style="display:block;width:100%;height:auto;"><figcaption style="padding:8px 10px;color:#64748b;font-size:13px;line-height:1.6;">代码审计、调用链与关键函数定位示意</figcaption></figure>]]></description><guid isPermaLink="false">272</guid><pubDate>Sun, 11 Oct 2026 22:35:12 +0000</pubDate></item></channel></rss>
