| name | open-redirect-testing |
| description | 检测开放重定向风险;当目标存在含重定向参数的 URL(url=、redirect=、next=、return=、goto=)时触发;适用于登录后跳转、OAuth 回调、链接中转等场景。 |
| when-to-use | 当目标 URL 含重定向参数(url=、redirect=、next=、return=、goto=),如登录后跳转、OAuth 回调、链接中转时 |
| allowed-tools | bash,read_file,list_files,rg |
| user-invocable | false |
开放重定向检测
成因引用
url / next / return / redirect 参数(用户可控的跳转目标字符串)→ 服务端跳转 sink(HTTP Location 头 / <meta http-equiv=refresh> / 前端 window.location 或 location.href 赋值 / OAuth redirect_uri 回调)= 开放重定向。一句话讲清成因:服务端把用户递交的字符串原样或弱校验后写入跳转目的地,攻击者借受信任域名外观把受害者引到恶意域。业务命名不可作筛选——不能用"参数名是 next 才查、callback_url 才查"作为是否检测的依据;任何参数只要最终流向 Location 头 / meta refresh / JS 跳转 / OAuth 回调,就属于此 sink 语义。同一站点经常出现 lang=zh&go=...、from=mobile&page=... 这种"非典型命名但实际触发跳转"的形态。详见同根目录 pentest/web-security-testing/SKILL.md 漏洞成因图谱 · 开放重定向行(不在本 skill 重复成因)。
触发线索(基线检查项)
以下是已知的常见开放重定向触发线索,作为基线起点而非必检硬清单:
- 适用且已完成 → 标注
[x] done
- 明确不适用 → 标注
[-] n/a (原因)
- 基线未列出但实际发现 → 新增条目并标注
[+] added (来源)
基线触发线索按"sink 语义"分类(不按业务命名):
- HTTP Location 头 sink:响应 30x 状态且
Location 头中出现请求参数原样回显或解码回显;登录后跳转、登出后跳转、表单提交后跳转、404 / 401 重定向。
- meta refresh / 前端跳转 sink:响应 HTML 含
<meta http-equiv="refresh" content="0;url=...">;前端 JS 出现 location.href = qs.get('next')、window.location.replace(params.url)、document.location = ...。
- OAuth 回调 sink:
redirect_uri、callback_url、return_url 参与 OAuth/OIDC 流程;服务端是否对回调 URI 做精确白名单匹配存疑。
- 链接中转 / 短链 / 跳板 sink:
/redirect?u=、/go?to=、/jump?dest=、短链解码后跳转。
- 业务上下文中的隐式跳转:分享链接、邮件中的"立即查看"、第三方登录回调、扫码登录回调、文件下载跳转(
/download?u=...)。
- 代码模式:后端代码出现
res.redirect(req.query.next)、HttpResponse.Redirect(Request["url"])、return RedirectResult(model.ReturnUrl)、response.sendRedirect(request.getParameter("u")) 等"用户可控字符串直接进 sink"形态。
思考检查点
加载本 skill 时按这些问题思考:
- 这个参数的字符串最终去了哪?是 Location 头、meta refresh、还是前端 JS 赋值?三种 sink 的检测方式不同。
- 服务端做了什么校验?协议白名单 / 域名白名单 / 相对路径限制?校验位置是否能被编码/转义绕过?
- 浏览器实际跟随了吗?只看 HTTP 响应里 Location 头含外部域不够,要看浏览器是否真的把用户带到外部站点。
- OAuth 回调场景下,重定向能不能挟带 token / authorization code?开放重定向 + OAuth = 凭证泄露而非"仅跳转"。
- 跨子系统:主站、移动端 H5、管理后台、第三方接入回调,各自的跳转校验是否独立实现?任一处都可能存在差异。
前置条件与安全边界
- 仅在授权环境测试。
- 单参数最多 10 次请求,绕过尝试不在生产环境真实用户路径上构造。
- 重定向目标使用自有受控域名(如
redhaze.test、oastify.com 等可控域),不使用真实恶意站点。
- 不向真实用户分发 PoC 链接,闭环验证仅在测试浏览器内自行点击完成。
检测步骤
Step 1:重定向参数与 sink 识别
定位包含 URL/路径的参数与对应 sink:
- 常见参数名(仅作起点):
url、redirect、next、return、returnTo、goto、continue、dest、redir、callback、u、to
- 观察响应:是否有 30x + Location 头?是否有 meta refresh?是否有前端 JS 跳转?
- 对响应里包含输入字符串的所有位置追踪,识别真正的 sink。
Step 2:直接外部 URL 测试
将参数值替换为外部 URL:
https://evil.com
http://evil.com
//evil.com(协议相对 URL)
Step 3:绕过测试
若直接外部 URL 被拒绝,尝试绕过:
| 绕过技术 | payload |
|---|
| 协议相对 URL | //evil.com |
| 反斜杠 | \/\/evil.com、/\evil.com |
| @ 符号 | http://trusted.com@evil.com |
| 子域名拼接 | http://trusted.com.evil.com |
| URL 编码 | %2f%2fevil.com |
| 双重编码 | %252f%252fevil.com |
| Tab/换行 / CRLF | http://evil%09.com、http://evil%0d%0a.com |
| 数据 URI / JS 协议 | data:text/html,<script>location='http://evil.com'</script>、javascript:alert(1) |
| Null 字节截断 | https://trusted.com%00.evil.com |
| 白名单子域绕过 | evil.target.com.attacker.com、target.com.attacker.com |
Step 4:闭环确认
- 构造完整的请求 URL。
- 在浏览器(或 headless 浏览器)中访问。
- 确认浏览器地址栏最终到达攻击者控制的外部域名。
- OAuth 场景下额外确认回调是否携带 authorization code / token 到攻击者域。
示例库
正例形态(代码层根因)
res.redirect(req.query.next)(无校验)— 经典 next 参数注入 Location 头(next-param-location-header-redirect)
response.sendRedirect(request.getParameter("url")) + Java Servlet — 同 sink,命名不同(next-param-location-header-redirect)
- OAuth 白名单匹配为
redirect_uri.startswith("https://app.target.com") — 可被 https://app.target.com.attacker.com 绕过(oauth-redirect-uri-wildcard-bypass)
- 前端
window.location.href = new URLSearchParams(location.search).get('to') — DOM 型开放重定向(js-locationhref-open-redirect)
窄化反例(必须避免)
以下是开放重定向维度的典型窄化误判:
- "参数名不是 url/next/redirect → 跳过" — 错。按 sink 语义(任何参数最终流向 Location 头 / meta refresh / JS 跳转 / OAuth 回调)筛选,不按命名筛选;
lang=zh&go=... 也可能命中。
- "只测了一个跳转端点 → 全站安全" — 错。登录、登出、OAuth 回调、短链、分享、404 重定向往往是独立模块,跨端点必须独立测。
- "白名单匹配
*.target.com → 安全" — 错。子域拼接 target.com.attacker.com、@ 符号 target.com@attacker.com、CRLF 注入都可能绕过;白名单要按"域名结构"严格解析,而非字符串匹配。
- "看起来是站内跳转 → 安全" — 错。需验证最终 Location 是否真为同源;浏览器跟随多次跳转后才到达最终域时,中间链路也可能被劫持(连环重定向)。
- "Location 头里参数被 URL 编码了 → 不可利用" — 错。浏览器会解码 Location 头,必须用浏览器实测而非 grep HTTP 响应判定。
反例义务(必须遵守)
为什么这里是「必须」:反例义务属于交付契约——"该子系统无开放重定向"或"已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全"。
写"未发现开放重定向"或"已防护"前,产物必须包含:
- 测过的开放重定向候选端点完整清单(按 sink 语义枚举,不按参数名筛选;含登录后跳转、登出跳转、OAuth 回调、短链/中转、分享链接、404/401 重定向、前端 JS 跳转)
- 每个端点测过的 payload 类型(直接外部 URL、协议相对、@ 符号、子域拼接、URL 编码、CRLF、JS/data 协议、Null 字节)
- 每个端点的浏览器实测证据(最终地址栏域名、响应 Location 头)
清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围。
特别警示:最容易漏的是"白名单匹配为字符串前缀/后缀"和"前端 DOM 型跳转"。前者用子域拼接 / @ 绕过可命中;后者在 HTTP 响应里看不到 Location 头,只能在浏览器里看 DOM 行为,grep 法会全部漏报。
闭环验证要求(必须遵守)
通用闭环口径见同根目录 common/closure-verification.md(技能表 path 列同一抽取根下,需要时 read_file 读取)。核心:完整证据链才判 confirmed,中间信号最多 suspected。本漏洞特有要点:
- 仅凭"参数值出现在 Location 头中"不得判 confirmed,必须确认浏览器实际重定向到外部域名(看地址栏)。
- DOM 型重定向要在浏览器中实测,HTTP 抓包无法证伪也无法证实。
- OAuth 场景需评估重定向是否能导致 authorization code / token 泄露到攻击者域;只到外部域但未带凭证 → 仍 confirmed 但风险等级较纯 OAuth 凭证泄露低,需在结论中区分。
判定标准
| 现象 | 判定 |
|---|
| 浏览器实际重定向到攻击者控制的外部域名(地址栏证据) | confirmed |
| OAuth 回调被引到外部域且携带 authorization code / token | confirmed |
| Location 头包含用户输入但浏览器未跟随(如 JavaScript 重定向被 CSP 阻止、或被中间页拦截) | suspected |
| 重定向目标被严格白名单限制,所有绕过均失败且覆盖完整 | not vulnerable |
| 覆盖清单不完整(如未测 DOM 型跳转 / 未跨子系统) | partial-coverage |
修复建议
- 对重定向目标使用严格白名单域名校验(按域名结构解析,非字符串前后缀匹配)。
- 仅允许相对路径重定向(以
/ 开头,且禁止 // 与 \\ 等协议相对形态)。
- 在 OAuth 场景中严格校验
redirect_uri 的精确匹配(包含协议、host、port、path),禁止通配符或前缀匹配。
- 对跨域重定向链接添加中间确认页("您即将离开本站,目标为 X"),让用户感知。
- 前端 JS 跳转前对目标做同源检查,避免直接把 URL query 写到
location.href。