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