safe-url
用户可控跳转目标守卫,含 safeExternalUrl / safeInternalPath 两个纯函数(auto-import)。任何「用户可控值(URL query / 后端透传字段 / location.hash)→ location.href / location.assign / window.open / navigateTo」的赋值前必须先过守卫——这是 standards client-security 硬规则「用户可控跳转目标必须过 scheme 白名单」的官方实现,不必再各项目手抄同款正则。
两守卫互补,按「目标应该去哪」选用:
| 场景 | 用 | 原因 |
|---|---|---|
外链中转页(?url= 倒计时跳转)、后端透传的站外跳转字段 | safeExternalUrl | 目标本来就是站外 http(s) 绝对地址 |
登录后 ?redirect= 回跳、返回来源页 | safeInternalPath | 目标必须留在站内,绝对外链也要拦 |
两者均为纯函数、无 IO,SSR / CSR 皆可调用。
API
| 函数 | 签名 | 说明 |
|---|---|---|
safeExternalUrl | (raw: unknown, fallback = '/') => string | 仅放行 http:// / https:// 绝对地址(大小写不敏感、trim 后判定),其余回退 fallback |
safeInternalPath | (raw: unknown, fallback = '/') => string | 仅放行以单个 / 开头的站内绝对路径,其余回退 fallback |
safeExternalUrl
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
raw | unknown | — | 待校验的跳转目标(通常来自 route.query,类型未知,数组 / undefined 都可能) |
fallback | string | '/' | 校验不通过时的回退地址 |
返回值
string —— 通过校验时返回 trim 后的原值;不通过时返回 fallback。
判定规则
trim 后大小写不敏感匹配 ^https?:// 才放行;以下一律回退 fallback:
- 危险 scheme:
javascript:/data:/vbscript:等(赋给location.href会执行 → DOM XSS) - 协议相对地址(
//evil.com)、相对路径 - 空值、非字符串(如 query 数组)
使用示例
// 示例 1:外链中转页(?url= 完全用户可控)
const url = safeExternalUrl(route.query.url)
location.href = url
// 示例 2:自定义回退地址
window.open(safeExternalUrl(raw, '/404'))
// 示例 3:后端透传的跳转字段同样要过守卫
location.href = safeExternalUrl(res.jump_url)
// 非法输入一律回退(默认首页 '/')
safeExternalUrl('javascript:alert(1)') // → '/'(危险 scheme)
safeExternalUrl('//evil.com') // → '/'(协议相对)
safeExternalUrl('foo/bar') // → '/'(相对路径)
safeExternalUrl(undefined) // → '/'(空值 / 非字符串)
safeExternalUrl(' HTTPS://example.com/a ') // → 'HTTPS://example.com/a'(trim + 大小写不敏感放行)
safeInternalPath
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
raw | unknown | — | 待校验的跳转目标(通常来自 route.query,类型未知) |
fallback | string | '/' | 校验不通过时的回退地址 |
返回值
string —— 通过校验时返回 trim 后的原值;不通过时返回 fallback。
判定规则
trim 后必须以单个 / 开头(正则 /^\/(?![/\\])/)才放行,如 /mine、/movie/detail/1?tab=a;以下一律回退 fallback:
- 协议相对地址(
//evil.com)、反斜杠绕过(/\evil.com)→ 开放重定向 - 绝对外链(
https://evil.com)、危险 scheme - 相对 / 裸路径、空值、非字符串(含 query 数组)
使用示例
// 示例 1:登录后回跳(?redirect= 用户可控,必须留在站内)
await navigateTo(safeInternalPath(route.query.redirect))
// 示例 2:返回来源页(referrer 同属用户可控),自定义回退到 /mine
router.push(safeInternalPath(fromPath, '/mine'))
// 示例 3:非法输入一律回退
safeInternalPath('//evil.com') // → '/'(协议相对)
safeInternalPath('/\\evil.com') // → '/'(反斜杠绕过)
safeInternalPath('https://evil.com') // → '/'(绝对外链,站内回跳必拦)
safeInternalPath(undefined) // → '/'(空值 / 非字符串)
safeInternalPath('/movie/detail/1?tab=a') // → '/movie/detail/1?tab=a'(放行)
攻击向量说明
外链中转页、分享回跳、登录后 ?redirect= 的目标 100% 来自参数、100% 攻击者可构造。守卫拦截的具体向量:
| 攻击向量 | 恶意输入示例 | 不设防的后果 | 守卫行为 |
|---|---|---|---|
| 危险 scheme | ?url=javascript:alert(1) | location.href 赋值会执行脚本(DOM XSS);data: / vbscript: 同理 | 两函数均回退 fallback |
| 协议相对地址 | ?url=//evil.com | 继承当前协议直接跳到站外(开放重定向) | 两函数均回退 fallback |
| 反斜杠绕过 | ?redirect=/\evil.com | 浏览器把 \ 归一化为 /,等效 //evil.com 跳站外 | safeInternalPath 回退 fallback |
| 绝对外链混入站内回跳 | ?redirect=https://evil.com/phish | 登录成功后用户被带去钓鱼站 | safeInternalPath 回退 fallback |
⚠️ 白名单是正解,别用黑名单修补。「以
//开头就补https:」这类黑名单式修补防不住编码绕过与新变体;两函数都是白名单判定(只放行明确合法的形态),其余一律回退。
注意事项
- 两函数均已 auto-import,也可显式
import { safeExternalUrl, safeInternalPath } from '@swift-web/framework'。 - 纯函数、无 IO,SSR / CSR 皆可调用(无需
onMounted/IS_CLIENT守卫)。 - 回退值
fallback自身不做校验,请传写死的安全常量(如'/'、'/404'),不要再传用户可控值。