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'),不要再傳使用者可控值。