登录接口读取 next=https://attacker.example,成功后直接写入 Location。密码验证可能完全正确,用户仍被站点带到了攻击者控制的页面。重定向既涉及 HTTP 语义,也涉及输入边界,不能只把它当成便利的跳转函数。
本文讨论普通网页导航与表单提交。API 客户端、移动 SDK 和代理对重定向的处理可能不同,生产流程应针对实际客户端测试。
状态码决定方法是否继续
HTTP/1.1 303 See Other
Location: /account/
提交表单后使用 303,表达客户端应以 GET 获取另一个资源,适合 Post/Redirect/Get 流程。307 与 308 则保留请求方法和正文,不能把它们随意用于“提交成功后查看结果”,否则客户端可能向目标重复发送 POST。
301 与 302 有历史兼容行为,实际浏览器可能把 POST 改成 GET。需要保留方法时选择明确语义的 307 或 308,不应依赖客户端的隐式转换。永久迁移与临时调整也应分开,永久状态码可能被缓存,错误跳转的恢复比临时跳转更困难。
表单提交成功后用三百零三转到结果页,刷新结果页不会重新提交原表单,但原始 POST 的重复发送仍然可能发生。用户双击或网络重试要靠幂等机制解决,不能把 Post/Redirect/Get 当成交易去重。重定向只改变之后的导航方法,不保证服务器此前仅执行一次写操作。
需要把请求原样转发到新位置时,三百零七或三百零八保留方法和正文,这意味着目标将收到相同敏感输入。不能把认证表单永久重定向到未经确认的新域名,也不能假设所有客户端都会安全过滤认证头。跨来源重定向的凭据处理有具体客户端规则,生产 SDK 与浏览器都应测试,文档不应承诺任意客户端一致。
永久状态码还影响回滚。浏览器或中间缓存可能记住迁移,服务端恢复旧路由后用户仍然前往新地址。初次不确定的迁移可以先用临时策略验证,再切换永久策略;但不能长期让同一资源摇摆。发布计划需要包含缓存影响与旧地址维护时间,而不只是更新一个响应码。
回跳目的地应属于业务流程,而不是任意 URL
假设登录入口允许回到文章编辑器或账户设置。相比接收任意 next 地址,可以接收有限目的地标识,再由服务端映射到固定路由。这样客户端表达意图,服务端决定地址。若确实需要保存深层路径,就把它限制在明确命名空间,并排除登出、外跳和其他改变状态的路由。仅仅同源不意味着适合登录成功后访问。
状态改变接口不应通过 GET 触发。否则一个合法同源回跳仍可能意外执行删除、解绑或退出。重定向验证无法补救这类路由设计。业务允许列表和正确 HTTP 方法需要同时成立;回跳目的地还应重新执行权限验证,因为用户登录成功不代表拥有目标资源权限。目标不存在时返回稳定账户页或清楚的缺失提示,不无限重试登录。
对携带状态参数的登录流程,应把目的地与一次性状态绑定,避免在用户认证过程中被另一个请求替换。状态值可以指向服务端保存的流程上下文,不必把全部目的地信息暴露在 URL 中。生成和验证应遵守所用认证协议的要求,不能用一个普通查询参数冒充防伪状态。这里的目标是保持登录前后意图一致,而不是只检查最终字符串。
function safeNext(value, origin) {
const base = new URL(origin);
if (base.username || base.password) throw new Error('Invalid trusted origin');
const fallback = new URL('/account/', base).href;
try {
const target = new URL(value || '/account/', base);
if (target.origin !== base.origin || target.username || target.password) return fallback;
return target.href;
} catch {
return fallback;
}
}
不要用 startsWith('/') 作为唯一检查,//other.example 是协议相对 URL;字符串包含本站域名也不足以验证来源。使用 URL 解析器比较 origin,可以同时覆盖协议、主机与端口。但如果业务只能回到少量页面,路径允许列表比任意同源路径更容易管理。
origin 来自可信站点配置,并且不能包含 username 或 password;配置错误立即失败。目标即使同源,携带 userinfo 也回退,不输出含凭据的导航地址。返回值保留已验证对象的绝对 href。例如 https://example.dev//other.example/path 的来源仍然是本站;若只返回它的 pathname,//other.example/path 再作为 Location 被解析就会换源。验证以后不重新拼成相对字符串,是保持来源保证的一部分。这个函数仍是来源边界示例,正式登录流程还需上述路由与查询允许规则。
同源路径也可能是另一条开放重定向接口,导致链式绕过。登录回跳应避免指向可任意外跳的路由,并限制嵌套参数。路径中的控制字符或异常编码不应原样写进响应头;成熟框架的头 API 比手工拼接 HTTP 报文安全。
URL API 能正确识别 origin,却不会知道哪些站内路由危险。解析后执行来源比较,再执行路径允许规则,最后通过框架响应 API写入 Location。输入包含编码分隔符或特殊字符时,不要在验证后又进行另一轮自定义解码,否则验证对象与最终访问对象可能不同。明确一次解析与规范化边界,减少各层重复处理。
路径前缀检查也有陷阱。允许 /account 前缀可能同时允许 /account-redirect,若后者是外跳接口就绕过预期。按路径段或路由标识匹配,比任意字符串前缀更可靠。查询参数要按目标路由需求筛选,不把嵌套 next、令牌或认证码原样继承到所有目标。片段可以用于页面定位,但不应承担服务端权限决策。
多语言回跳需要保持有效路径。用户在中文编辑器登录后,不必为了验证方便全部送到英文首页。可以保存合法语言标识与资源标识,再生成目标地址;浏览器语言只影响缺省值,不覆盖用户已选择的页面。对于没有对应翻译的资源,应有明确回退,而不是连续语言跳转形成环。
外部跳转需求使用独立受控入口
有些产品确实需要跳到合作方控制台,不能用全部禁止外部来源来假装没有需求。可采用有限目的地标识与固定来源映射,逐项确认协议、主机和端口;合作方新增子域名要重新登记。若目标路径需要参数,按业务字段生成,不接受完整 URL 作为拼接材料。允许某个主机也不意味着允许其任意路径承担敏感回跳。
进入外部站点时可以明确告知目标名称,必要时提供确认,但提示不能替代来源验证。用户看到“即将离开”仍可能相信这是官方可信页面,因此受控跳转不应成为任意链接的信誉背书。对确实允许用户分享任意外部链接的功能,采用普通链接语义和独立风险提示,不与认证成功回跳混用。
错误目标的处理应固定且可观察。将拒绝原因分类为来源不允许、路径不允许或解析失败,不把包含令牌的原始值全部记入日志。聚合异常数量可以帮助发现攻击或客户端回归,但日志不能把安全输入变成长期凭据存储。对于合法链接突然被拒绝,错误分类与版本号足以定位规则变更。
最终审查重定向 helper 的调用点。一个 helper 本身严格,另一个路由却直接写 Location,整站仍然存在旁路。搜索所有目的地来源,明确哪些来自固定配置、哪些来自可信数据库、哪些来自用户输入,并优先检查最后一类。测试需要覆盖真实入口,不只测试工具函数,才能发现调用方在验证后重新拼接地址的问题。
对永久迁移也记录维护责任。域名到期、目的地撤销或合作关系结束后,旧跳转可能把流量送往不再可信的站点。定期校验目的地所有权与可达性属于链接生命周期的一部分。重定向安全不是一次字符串检查,而是从输入、请求语义到目标维护的一组持续契约。
域名迁移必须保留深层链接
将 www.example.dev 转到主域名时,应保留 pathname 与查询参数,而不是一律指向首页。片段不会发送给服务器,不能声称服务端可以重新构建所有客户端片段。协议升级要避免 HTTP 与 HTTPS 来回跳转,代理后面生成 URL 时必须使用可信的外部来源配置。
不要无条件相信客户端传入的 X-Forwarded-Host。如果上游代理没有清理头,攻击者可能影响绝对重定向地址。应用应明确哪些代理可信,并使用固定公开域名作为基准。
把每条迁移登记为源、目的地、状态码和原因,运行图检查发现环与过长链。目标需要是明确终点或已知下一跳,不允许未验证的自由文本。若 A 到 B、B 到 C,可以把 A 直接改到 C,但需要确认 B 没有添加必要参数或执行业务逻辑。纯内容迁移应尽量没有这类隐藏行为。
域名规则与路径规则组合时尤其要测。例如 HTTP 转 HTTPS、www 转根域名、旧 slug 转新 slug,可能产生三次往返,甚至因不同层规则相反而循环。统一计算最终规范地址,或明确每层职责,能减少链路。代理头只从可信上游获得,固定主域名配置通常比从请求头临时推导更安全。
查询中的敏感信息还可能进入浏览器历史、服务器日志和外部分析。不要把登录令牌从旧地址保留到新地址,之后再跳到第三方页面。必要流程使用短期一次性值,并在消费后清理地址。保留深层链接的便利原则,需要服从凭据最小暴露的边界。
通过登录流程保存意图而不是保存任意字符串
用户在编辑文章 A 时被要求登录。流程上下文可以保存资源标识、目标动作和语言,而不是完整的 next 字符串;认证完成后由服务端按当前路由规则重建地址。资源仍然需要存在,当前用户仍然需要具有编辑权限。认证证明用户是谁,不能自动证明此前保存的资源可访问。目标无权访问时应提供明确结果,不循环跳回登录。
上下文还要区分多个浏览器标签。标签一准备编辑 A,标签二准备查看 B,不能用一个全局会话字段让后来的意图覆盖先前流程。每次流程有独立状态并受有效期和协议校验约束,认证返回时消费对应上下文。一次性状态的具体生成与验证依所用身份协议实现,不自创一个“随机值看起来够长”的认证方案。
流程过期时回到安全入口并解释需要重新开始,不继续使用未经验证的 URL。已经消费的上下文再次收到回调,应按照认证库的重复与重放处理规则拒绝或查询既有结果,而不是重复执行账户绑定等副作用。重定向只是最终导航,认证事件与业务写入必须有自己的幂等和事务边界。
如果语言缺少对应页面,选择已定义的回退地址,同时保留可理解的资源意图。不要把浏览器默认语言覆盖用户手动选择,也不要依赖片段传递服务端状态。片段适合定位正文,不会随普通 HTTP 请求发送,服务端不能据此验证登录目标。将用户意图结构化,既简化来源检查,也能避免路由迁移后大量旧 next 字符串失效。
合作方目标要验证整个信任关系
允许 partner.example 还不够,如果该域名上有另一条接受任意外部地址的跳转接口,本站登录成功以后仍可能进入攻击者页面。固定来源之外,应选择固定目的地路径和有限业务参数,避免把嵌套 redirect 参数传给合作方。合作方控制的路径是否会继续跳转,需要通过文档与受控测试确认,不能假定同一公司域名下所有功能都适合承载认证回跳。
子域通配也需要所有权审查。某些子域由客户控制,某些旧子域指向已停用服务,允许整个域树会扩大实际目标集合。目的地登记表保留负责人、用途、批准路径与停用操作。合作结束后移除入口,旧映射返回稳定说明,不继续把流量送往可能变更所有者的站点。这个维护责任与一次 URL 解析正确性同样重要。
目标需要携带令牌时,应优先遵循成熟协议的授权流程,使用限定用途和有效期的交换机制,而不是把本站会话复制进查询参数。查询会进入历史、日志与某些引用来源,重定向链越长,暴露面越大。只有明确需要的字段才传递,错误路径也不能把完整认证响应写入页面或日志。
给永久迁移制定有限验证期
准备把整个文档域名迁移时,先建立旧路径清单和新地址映射,在预览或受控流量中使用临时策略验证深层路径、语言与查询。通过后再切换永久迁移,并记录长期维护的域名与证书责任。临时验证期不是无限摇摆;应预先定义验收条件、结束时间和失败处理,避免不同用户长期被送到不同版本。
测试浏览器已经缓存旧永久跳转的情形,说明回滚不一定立刻影响这些客户端。新旧站点同时可读时,canonical 与站内链接也要与迁移方向一致。把 HTTP 升级、www 规范化和路径迁移分别配置后,检查组合路径是否多跳。能在可信配置中一次计算最终目标时,可减少往返,但不能遗漏旧规则原本承担的参数转换。
迁移规则应限定需要迁移的方法和命名空间。公开文章 GET 的迁移不应无意覆盖上传、认证与 webhook。对于必须保留 POST 的接口迁移,确认目标确实支持同一正文协议、权限验证和重复请求处理。三百零七或三百零八只是要求客户端保留方法,不证明目标接口与原接口在业务上可互换。
用受控终点证明真实副作用边界
测试端点可以记录收到的方法、路径和合成正文摘要,客户端在不含真实凭据的条件下请求不同重定向。不要使用生产支付或删除接口来验证方法是否保留。每个测试明确是否自动跟随跳转,以及所用 SDK 版本与配置,因为工具选项可能主动覆盖默认行为。记录首跳与最终请求,让方法变更有直接证据。
负向样本不仅测试拒绝值,也测试验证后的调用链。一个合法路径通过 helper 后,调用者再拼接另一个 next 参数,仍然可能制造旁路。可以给真实登录入口使用合成流程,检查最终 Location 与目标收到的请求,而不是只断言 helper 返回字符串。来源规则、路由允许规则与参数投影分别覆盖,再执行组合测试。
让错误恢复保持可理解且不暴露输入
无法解析的目的地落到稳定账户页,但可以展示“原目标不可用”的普通状态,不必回显原始 URL。合法资源已经删除时,与来源被拒绝的情形内部分类不同;对外展示是否细分取决于权限与用户需求。日志采用规则版本和错误类别,必要样本经过脱敏与长度限制。安全默认目标提供恢复路径,不代表所有错误都应该静默消失。
最后检查默认页面本身不会继续跳转到同一个失效流程。常见环是账户页检测缺少某项状态,重新送去登录,登录完成再送回账户页。测试无权用户、过期上下文与缺失资源,确保每种失败最终有一个稳定可读的页面。请求语义、目标来源和恢复终点三者共同成立,重定向才真正完成用户任务。
用方法与边界共同测试
对 GET 与 POST 分别请求每种跳转,记录最终方法、正文是否保留、状态码与 Location。用本地测试端点回显收到的方法,避免根据浏览器地址栏猜测。
负向输入应包括完整外部 URL、协议相对 URL、不同端口、混淆域名、非法 URL 与指向本站另一条跳转的路径。迁移测试还要覆盖文章深链接、查询参数与多语言路径。最后检查重定向环与多跳链,减少重复往返,并保证意外失败时能回到稳定默认页面。
流式上传还有正文能否重放的问题。客户端已经逐块读取本地输入,收到保留方法的重定向后,未必拥有可再次读取的同一份流。某些客户端会拒绝跟随,某些需要可重放的数据来源;具体行为取决于其实现。测试使用小型合成流和当前 SDK,确认发生的是重发、拒绝还是需要人工重启,不能从普通字符串 POST 的结果推导大文件上传。
若上传入口长期会迁移,更稳妥的协议可能先取得受控上传地址,再向最终目标发送正文,而不是让已经开始的上传不断跳转。取得地址仍需认证、对象权限及来源验证,不能变成任意上传代理。失败恢复保留上传身份与已确认阶段,判断是否需要继续或重新开始;方法没有改变,并不证明服务端已经收到完整文件,也不证明重复上传不会创建另一份对象。