打开 SSRF 防护后,为什么 302 不能自动跟随

首跳安全不代表下一跳安全;严格模式把隐式跳转改成可检查的显式决策。
摘要| 严格 SSRF 模式禁止自动跟随 3xx,不是把重定向当成错误,而是拒绝让一个已验证的公网 URL 替尚未验证的下一跳背书。当前源码使用独立 RestClient 返回原始 302;调用方读取 Location、解析相对地址、限制跳数,并把每一跳重新送回同一套目标校验。
✦ 危险的不是 302,而是被跳过的第二次校验
服务端请求先访问一个正常公网地址,对方返回 302,把 Location 指向 127.0.0.1、内网数据库管理页或云元数据地址。如果 HTTP 客户端自动跟随,应用只校验了第一跳,真正发出第二次请求时却没有再问目标是否安全。

只校验入口,会让重定向成为绕过目标验证的第二条路。
核心判断| Location 只是远端返回的一段不可信输入。它不能继承首个 URL 的安全结论。
✦ 当前源码用两个客户端保留兼容与严格语义
Microi吾码AI 当前工作区的 DiyHttp 没有把所有历史集成一次性切断。默认 _sharedClient 继续保留旧行为;严格模式使用单独的 _strictSsrfClient,并明确设置 FollowRedirects=false。
只有主租户开启 SsrfProtectionEnabled,或可信控制面把本次请求标记为 RequireSsrfProtection,才进入严格路径。
private static readonly RestClient _strictSsrfClient =
new RestClient(new RestClientOptions {
ThrowOnAnyError = false,
FollowRedirects = false,
Timeout = Timeout.InfiniteTimeSpan
});

严格客户端禁止自动跳转;兼容客户端维持存量行为。
✦ 严格模式下,302 是完整响应,不是神秘失败
关闭自动跟随后,请求仍然成功到达首个服务器,3xx 状态、Location 和响应头会原样回到调用方。关键集成应使用 GetResponse、PostResponse 或 PatchResponse,而不是只拿 Content 的字符串方法;这样才能分清传输错误、重定向、最终 2xx 与业务失败。
var response = V8.Http.GetResponse({
Url: currentUrl,
Timeout: 30,
Headers: safeHeaders
});
if (response.ErrorMessage) return { Code: 0, Msg: response.ErrorMessage };
if (response.StatusCode >= 300 && response.StatusCode < 400) {
var location = getHeader(response.Headers, 'Location');
// 把 location 当作新的不可信目标处理
}
不要误判| HTTP 302 既不等于业务成功,也不等于网络失败;它表示下一步需要调用方做安全决策。
✦ 逐跳跟随要同时解决地址、次数和凭据
安全实现至少同时约束地址、跳数与凭据,缺一不可。
- 基于当前 URL 解析相对 Location。
- 只接受 HTTP(S) 协议。
- 把下一跳作为新请求重新触发 SSRF 校验。
- 设置很小的最大跳数,并记录已访问地址阻断循环。
- 跨主机时默认移除 Authorization、Cookie、签名头和业务秘密。
for (var hop = 0; hop < 4; hop++) {
var r = V8.Http.GetResponse({ Url: currentUrl, Timeout: 30 });
if (r.StatusCode < 300 || r.StatusCode >= 400) return verifyFinal(r);
var nextUrl = resolveAgainst(currentUrl, readLocation(r.Headers));
if (seen[nextUrl]) return { Code: 0, Msg: '重定向循环' };
seen[nextUrl] = true;
currentUrl = nextUrl; // 下一轮会重新经过严格 SSRF 校验
}
return { Code: 0, Msg: '重定向次数超限' };

每一跳都是一次新请求,因此必须重新验证目标。
✦ 301、302、307、308 也不能一概而论
重定向状态还涉及方法语义。301/308 常表达长期迁移;302/303 的客户端历史行为可能把 POST 改成 GET;307/308 要求保留方法和请求体。对创建订单、扣款、发券等写请求,盲目重放比一次失败更危险:既可能把秘密送到新主机,也可能重复产生副作用。

是否跟随、是否保留方法、是否携带请求体,需要按契约逐项决定。
- 稳定迁移优先更新受控配置,不长期依赖运行时跳转。
- 写请求必须确认对方支持幂等键,再考虑显式重发。
- 跨主机跳转重新选择允许的请求头,不复制整包 Headers。
- 最终仍要检查 StatusCode、ErrorMessage 与业务成功字段。
✦ 本轮 13/13 测试证明了什么
我从当前工作区重新运行 DiyHttpSsrfCompatibilityTests 与 ApiEngineHttpResponseContractTests:13 个用例通过,失败 0、跳过 0。前者锁定兼容模式、严格拦截和精确主机白名单;后者证明 HTTP 响应契约能保留 302、Location 与多值响应头,并拒绝不安全的 Location 形式。

真实本地测试 13/13;TRX、源码范围与 SHA-256 已归档。
证据边界| 测试证明当前控制流与响应契约符合预期;它不代表任意第三方站点、代理链或生产 DNS 都已完成在线端到端验收。
✦ 真正的结论:把隐式网络行为变成可审计决策
自动跟随追求便利,严格 SSRF 追求的是每个目标都可解释。禁用 FollowRedirects 让 3xx 留在证据链里:调用方看见状态码,读取 Location,决定是否继续,并确保下一跳仍受协议、DNS、IP、白名单、凭据和跳数边界约束。

安全不是一个开关,而是一条逐跳可验证、最终可判断的链。
结论| 严格模式不是拒绝重定向;它拒绝未经重新验证的自动重定向。
本文7张竖版技术图卡背景由AI生成;流程图、矩阵和测试卡片为基于当前工作区源码与真实测试结果的确定性渲染。
更多推荐




所有评论(0)