URL重定向的2026年真相:你校验的是“跳转目标”,浏览器执行的是“另一个跳转目标”"

admin 2026-11-06 03:07:49 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

URL重定向的2026年真相:你校验的是“跳转目标”,浏览器执行的是“另一个跳转目标”

原创

www.klsec.com
www.klsec.com

昆仑AI安全实验室

2026年11月5日 01:14
广东

去阅读

2026年7月,Authorizer披露了一个CVE,CVSS评分9.3。漏洞的描述读起来像一句黑色幽默:

/authorize端点把已登录用户的access\_token、id\_token和refresh\_token,直接作为查询参数,附加在攻击者提供的redirect_uri后面,然后返回302。

不需要XSS,不需要CSRF,不需要任何“绕过”。攻击者构造一个链接,受害者点击,授权码和令牌就出现在攻击者的服务器日志里。client_id从哪里来?Authorizer提供了一个未认证的GraphQL端点/graphql?query={meta{client_id}},直接告诉你。

这个漏洞的修复方式更值得玩味。Authorizer在v2.0.1里给oauth_login、verify_email、magic_link_login、forgot_password、invite_members、oauth_callback六个处理器都加上了validators.IsValidOrigin()检查。唯独/authorize漏掉了。但即使补上这个检查,如果运维没有显式配置ALLOWED_ORIGINS,它的默认值是[\"*\"]——检查存在,但永远返回真。

这就是2026年URL重定向漏洞的精确切片:危险不在于“有没有做跳转校验”,而在于“校验的结论有没有被强制执行到跳转发生的那一刻”,以及“校验的host和浏览器理解的host是不是同一个东西”。

一、为什么单独是Low,链式是Critical:开放重定向的“接驳器”本质

一个只能把用户从A站跳到B站的漏洞,在SRC里通常被定为Low或Info。它看起来只是钓鱼的前置步骤——“用户看到链接是target.com,点进去被跳到了evil.com”。这个危害是真实的,但定价不高,因为它需要受害者的配合,而且钓鱼本身不是一个技术漏洞。

2026年的现实是:开放重定向单独出现时确实不致命,但它是几条最高危攻击链上不可替代的“接驳器”。 Decepticon的攻击手册把这一点写得很直接:开放重定向作为OAuth redirect_uri白名单绕过的中继,导向授权码泄露和账户接管(ATO);作为SSRF中绕过DNS固定或IP白名单的302跳转,导向内网和云元数据访问;配合Tabnabbing完成原标签页的视觉劫持。在这三种链式场景中,严重度从Low直接跳到Critical 9.0。

OAuth场景的接驳逻辑最干净。 身份提供方(IdP)的白名单校验的是redirect_uri的初始值。攻击者提交的redirect_uri是一个在目标应用域名下、但存在开放重定向漏洞的端点。IdP看到host是app.example.com,放行。授权码被发送到app.example.com/redirect?url=https://evil.com。这个端点返回302,把浏览器(连同URL fragment或query里的授权码)导向evil.com。IdP的校验是“初始host在不在白名单”,攻击者利用的是“初始host在不在白名单,和授权码最终到达哪里,是两件事” 。Backstage的CVE-2026-73563就是这个模式的精确标本:通配符isMatch可以跨URL组件边界匹配,攻击者控制的redirect\_uri只要路径里包含受信任主机名的后缀,就能通过白名单,在受害者完成OAuth流程后接收授权码。

SSRF场景的接驳逻辑同样清晰。 服务端的SSRF防护通常校验“初始URL的host是不是内网IP”。如果初始host是一个公网可达、但存在开放重定向的目标,防护通过。服务端跟随302,请求到达169.254.169.254或127.0.0.1:8080。SSRF防护的假设是“请求会到达初始URL指定的host”,开放重定向让请求到达了另一个host。 Grafana的CVE-2026-22638把这两条链接在了一起:客户端路径遍历+开放重定向首先导致XSS;如果安装了Image Renderer插件,同一个开放重定向可以被用来实现full-read SSRF。

二、解析器差异:你校验的host,和浏览器跳转的host,不是同一个字符串

开放重定向在链式利用中的价值,依赖于一个前提:攻击者能让“校验组件看到的URL”和“执行组件(浏览器/HTTP客户端)理解的URL”产生分歧。 2026年最有效的绕过,几乎全部落在这个分歧上。

armourinfosec的实测数据把这个分歧展示得极其具体。他们把同一个URL字符串分别交给PHP 8.4的parse_url()、Python 3.13的urlsplit().hostname、Node 24的new URL()(WHATWG标准,浏览器实现的就是这套),然后看各自解析出的host是什么。允许列表是app.example.com。结果里有一行是致命的:

https://evil.com\\@app.example.com/

PHP parse_url 的host:app.example.com
Python urlsplit 的host:app.example.com
浏览器(WHATWG)的host:evil.com

一个反斜杠,击穿了一个正确编写的服务端主机白名单。服务端做了它该做的事:解析URL、提取host、和白名单比对。它没有做错任何逻辑。它错在用了一个把\\当作userinfo普通字符的解析器,去校验一个浏览器会用WHATWG标准(\\等同于/)来执行的URL。浏览器看到的是https://evil.com/@app.example.com/,host是evil.com。服务端看到的是https://evil.com\\@app.example.com/,userinfo是evil.com\\,host是app.example.com。两边都“正确”地解析了,但解析出了两个不同的东西。

相对路径的绕过同样依赖解析器差异。如果一个校验器只拒绝“不以单个/开头”的输入,那么/\\evil.com、///evil.com、/\\/evil.com全部以单个/开头,全部通过校验。但浏览器解析Location: /\\evil.com时,把\\当作/,得到//evil.com,host变成evil.com;///evil.com直接被解析为host是evil.com的协议相对URL;/\\/evil.com在WHATWG下同样变成//evil.com。

空白字符的绕过更隐蔽。WHATWG解析器在解析URL之前,会移除所有ASCII制表符和换行符。攻击者提交?next=/%09/evil.com。校验器看到的是/%09/evil.com,以单个/开头,没有//,通过。应用经过URL解码后发出Location: /<TAB>/evil.com。浏览器先移除TAB,剩下//evil.com,跳转到evil.com。整个Payload里没有任何“不寻常”的字符——没有@,没有反斜杠,没有双重编码。 解码顺序(应用先解码、浏览器后剥离空白)本身就是一条绕过路径。

三、2026年实战:测开放重定向时,你在测什么

把上面的逻辑收拢,开放重定向的测试重点不是“找到一个接受URL参数的端点”,而是“找到一个端点,它的校验结论和跳转执行之间的语义不一致”。

第一,先区分“服务端跳转”和“客户端跳转”。 服务端跳转检查Location头;客户端跳转检查location.href、window.location、location.replace()这些赋值。客户端跳转的风险更高,因为浏览器执行的解析逻辑(WHATWG)和服务端校验逻辑(PHP/Python/Java的URL解析器)几乎必然存在差异。Decepticon的侦察建议很直接:在JS bundle里grep location.href =、window.location =、location.replace(,以及next、returnTo、callbackUrl这些变量名。

第二,把解析器差异当成一个独立的测试维度。 不要只测“https://evil.com能不能通过”。测https://[email protected](PHP/Python/浏览器都会解析为evil.com,但如果校验逻辑用了startsWith就会中招)。测https://evil.com\\@target.com(PHP/Python看到target.com,浏览器看到evil.com——这是最有价值的那条缝)。测/\\evil.com、///evil.com、/%09/evil.com。测IDN变体:https://ⓔⓥⓘⓛ.com/在PHP/Python里是“literal”,在浏览器里被规范化成evil.com;https://evil。com/(全角句号U+3002)在服务端字符串比较里不是evil.com,在浏览器里被IDNA映射成evil.com。

第三,优先找“回跳场景”的参数。 登录后的?next=、登出的?logout_redirect=、OAuth的redirect_uri、SSO的RelayState、支付回调的return_url、密码重置的success=。这些参数的业务语义就是“跳回去”,所以校验逻辑往往集中在“是不是我们的域名”,而“我们的域名”的判定方式,正是解析器差异的攻击面。

第四,把开放重定向当作链式利用的入口来评估,而不是孤立漏洞。 找到开放重定向之后,问三个问题:这个应用有没有OAuth/SSO流程,redirect_uri能不能指向这个端点?这个应用的服务端有没有SSRF防护,防护是否只校验初始host?这个重定向的目标URL会不会被写入Location头并且被浏览器以text/html渲染?第一个问题打开ATO链,第二个问题打开SSRF链,第三个问题打开XSS链。 单独提交“存在开放重定向”通常拿不到高价,把链走通才是定价依据。

四、防御:开放重定向的唯一正确姿势是“不根据用户输入做跳转”

2026年的这些CVE和解析器差异数据,指向一个防御原则:任何基于用户输入的跳转,都不应该由服务端或客户端直接执行。 如果业务必须支持“跳回去”,正确的做法是间接映射,而不是直接传递。

第一,不要传URL,传ID。 用户完成登录后要跳回/settings,让前端传?next=settings,服务端从一个硬编码的允许路径表里查这个ID对应的路径。settings不是URL,它不可能被解析成https://evil.com,也不可能因为反斜杠或空白字符而改变语义。把“用户控制的目标”从一个自由字符串,压缩成一个枚举值。 这是唯一从根本上消除解析器差异的方案。

第二,如果必须传URL,校验和跳转必须在同一个解析器上完成。 Authorizer的修复方式是给/authorize加上和其他六个处理器相同的IsValidOrigin检查。但更根本的要求是:校验时解析出的host,必须被传递到跳转执行的那一刻,并且执行时不能重新解析。 如果校验用PHP的parse_url,跳转时就不能让浏览器去解析一个“PHP认为安全”的字符串——因为浏览器不认PHP的解析规则。校验的结论和执行的输入,必须是同一个东西。

第三,默认值必须是“拒绝”,而不是[\"*\"]。 Authorizer的AllowedOrigins默认是[\"*\"],这意味着即使代码里加了检查,检查也永远返回真。Backstage的redirect_uri allowlist绕过之所以能成立,是因为isMatch的通配符可以跨组件边界匹配,而且没有显式协议的模式可以匹配 unintended protocols。白名单必须是精确的、带协议的、逐字符比较的,而不是通配符或前缀匹配。

第四,把开放重定向纳入“链式风险评估”的必检项。 一个单独的低危开放重定向,在存在OAuth或SSRF防护的环境中,可能是一条Critical链的起点。测试和修复的优先级,不应该由“这个漏洞单独有多严重”决定,而应该由 “这个漏洞能接驳到哪条链上” 决定。

写在最后

2026年的URL重定向漏洞,已经很少是“开发者忘了校验?url=参数”那种形态了。校验往往存在。问题在于:校验的host和浏览器理解的host不是同一个东西(反斜杠、@、空白字符、IDN),校验的结论没有被强制执行到跳转发生的那一刻(/authorize漏掉了检查),或者校验的默认值是“全部放行”([\"*\"])。

开放重定向的危险不在于“跳转”,在于它把信任的流向改了道。 OAuth的授权码本来要回到app.example.com,一个开放重定向让它先经过app.example.com再被302到evil.com。IdP的白名单校验的是“第一站的host”,攻击者利用的是“第一站会把乘客转送到哪里”。SSRF的白名单校验的是“初始请求的host”,攻击者利用的是“初始请求会跟随302去哪里”。

下次看到一个接受URL参数的端点,不要只问“它能不能跳到evil.com”。问:它的校验逻辑用的是什么解析器?跳转执行用的是什么解析器?这两个解析器对同一个字符串的理解,在哪里分叉? 分叉的地方,就是链式利用开始的地方。

严正声明

本文所述所有漏洞分析、CVE案例及测试技术,仅用于已获得明确书面授权的安全测试、代码审计、红队演练和安全研究场景。CVE-2026-54072(Authorizer)已在对应提交中修复,但运维必须显式配置ALLOWED_ORIGINS,默认[\"*\"]会使修复无效。CVE-2026-73563(Backstage)已在0.29.2中修复。CVE-2026-22638(Grafana)的XSS和SSRF链已在对应版本中修复。所有测试必须在SRC平台或客户授权的资产范围内进行,靶场环境应部署在隔离网络中。未授权扫描、测试、攻击行为违反《中华人民共和国网络安全法》第27条及《刑法》第285条,将承担法律责任。技术无罪,边界是法律。

    评论:0   参与:  0