跳转接口如果不校验目标,就是一个「用你的域名发出去的通行证」。
Demo 里 next=https://evil.example/steal 直接回了 302,Location 指向外站。
而 //evil.example 这种写法能绕过「必须 http 开头」的黑名单。
上一节泄露的是内部信息,这一节借的是你的域名的可信度。
一、为什么这算漏洞
登录成功之后跳回原来要去的页面,这是个很常见的需求,所以接口往往写成 ?next=<目标地址>。
如果服务端不校验,这个接口就变成了「你给我一个地址,我让你的浏览器跳过去」。
听起来没什么,但关键在于:跳转是从你的域名发起的。
钓鱼链接写成 your-site.com/go?next=https://evil.example,用户看到域名是你,就不会怀疑。
点下去之后落到一个假的登录页,输的账号密码直接进了攻击者的库。
它还常被用来绕过邮件和聊天软件的外链检测——那些系统看到域名可信就放行。
二、两种绕过写法
最直白的载荷是 https://evil.example/steal,Demo 里朴素版回 302,Location 就是这个地址。
稍微聪明一点的过滤器会要求「必须以 http 开头」或者「必须包含本站域名」。
于是有了第二种写法://evil.example/steal。
它不以 http 开头,所以过了「http 开头」这条检查;
但浏览器解析它时,// 表示「同协议、另一个主机」——照样跳到外站。
还有更绕的:https://[email protected]/(用户信息那段)、
https://your-site.com.evil.example/(子域名伪装)。
黑名单永远列不全,因为这些写法是浏览器解析规则的一部分。
三、白名单,而且只认相对路径
Demo 的修正版只做一件事:目标必须以 / 开头、不能以 // 开头、不能含冒号。三条件一起,才允许跳。
结果三行里第一行 /dashboard 还是 302,
后两行 https://evil.example 和 //evil.example 都变成 400。
更稳的做法是根本不让外部传地址:参数只给一个「页面标识」,服务端自己映射到路径。
这样攻击者连拼接的机会都没有。
最后一条:如果确实需要跳到外站(比如第三方登录回跳),就维护一份明确的外站白名单,
并且跳转前给用户看一个确认页。
四、动手 Demo:三种目标,两种被放行
本机起一个跳转接口,把 next 直接写进 Location。
分别传站内路径、外站地址、双斜杠写法,看回什么;再加一版只许站内的校验。
nxt = q.get(u'next', [u'/'])[0]
if whitelist:
ok = (nxt.startswith(u'/') and not nxt.startswith(u'//')
and u':' not in nxt)
if not ok:
return 400
return 302, [(u'Location', nxt)]
实跑输出:
--------------------------------------------------------------------
开放重定向:你的域名帮别人发了一张通行证
--------------------------------------------------------------------
目标 next 参数 不校验 只许站内
站内相对路径 /dashboard 302 302
外站绝对地址 https://evil.example/steal 302 400
双斜杠协议相对 //evil.example/steal 302 400
外站绝对地址 跳去了 https://evil.example/steal
双斜杠协议相对 跳去了 //evil.example/steal
第二、三行都是 302,`Location` 直接指向 evil.example——
浏览器会真的跳过去。可用户看到的链接是 `your-site.com/go?next=...`,域名是你的,他不会怀疑。
这一招常被用来做钓鱼的第一跳:链接看起来正规,落地页却是假的登录页。
第三行 `//evil.example` 要特别注意:它**不以 http 开头**,所以「只允许 http 开头」那种黑名单挡不住它——浏览器会把它当成「同协议、另一个主机」。
朴素版三行全是 302。
第二、
三行打出来的 Location 分别是 https://evil.example/steal 和 //evil.example/steal——
都指向外站。
修正版第一行还是 302(站内的正常跳转),后两行 400。三个条件各挡一种写法。
第三行 //evil.example 是最容易漏的那一种:它看起来像站内路径(以 / 开头),
实际是「另一个主机」。
所以判断「是不是站内」不能只看开头有没有斜杠,还要专门排掉双斜杠和带冒号的形式。
30 秒实验
- 试试
https://[email protected]/,看浏览器的地址栏显示的是什么。 - 试试
/%2f%2fevil.example(编码过的双斜杠)能不能绕过。 - 想一想:为什么「跳转前给用户一个确认页」能降低危害?
常见误区
- 「只是跳转,又不执行代码,不算漏洞」——它借的是你的域名信用。
- 「限制必须 http 开头就够了」——
//evil.example不以 http 开头。 - 「只允许本站域名出现在参数里就够了」——域名可以写成子域名或用户信息段。
自测三问(点开对答案)
1. 开放重定向的危害是什么? —— 攻击者能用你的域名发出看起来可信的链接,把用户导向钓鱼页面。 2. 为什么 `//evil.example` 能绕过「必须 http 开头」的检查? —— 它以 `/` 开头,但浏览器把 `//` 解析成「同协议、另一个主机」。 3. 最稳的修法是什么? —— 不让外部传地址,参数只给页面标识,由服务端映射到路径。设置这一类讲完了。最后一类最阴:用户什么都没做错。
- ☐ (本节完,下一节 11.4.1 会话劫持)