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

参数

参数类型默认值说明
rawunknown—待校验的跳转目标(通常来自 route.query,类型未知,数组 / undefined 都可能)
fallbackstring'/'校验不通过时的回退地址

返回值

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

参数

参数类型默认值说明
rawunknown—待校验的跳转目标(通常来自 route.query,类型未知)
fallbackstring'/'校验不通过时的回退地址

返回值

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'),不要再传用户可控值。