Files
a620906209 cb9c9a9385 security(auth): P0 認證安全強化
- Token 過期時間設為 7 天(sanctum.php expiration)
- prune-expired 每日排程清除過期 token 記錄
- Google OAuth redirect 改用 URL fragment(#token=)避免 token 出現在 server log
- SocialAuthController env() 改 config() 修正 config:cache 下 redirect 失效問題
- 登入端點加 rate limiting:member/provider throttle:5,1,admin throttle:3,1
- axios/coachAxios 加 401 response interceptor,token 過期自動登出
- 修正 migration SQLite 相容性(MODIFY COLUMN),Feature tests 從 7 通過恢復至 41
- 新增 AuthRateLimitTest(5 tests)驗證各角色頻率限制行為

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 01:54:40 +08:00

6.8 KiB
Raw Permalink Blame History

Context

CFDivePlatform 使用 Sanctum Bearer Token 認證,token 儲存於前端 localStorage。安全評估發現三個在當前架構下可直接修復的高危漏洞,不需要切換認證機制。此 change 針對性修補,範圍刻意控制在最小改動。

Goals / Non-Goals

Goals:

  • Token 設定有限期,降低洩漏後的持續暴露時間
  • 消除 Google OAuth redirect URL 中的 token 洩漏路徑
  • 對登入端點加入基本的暴力破解防護

Non-Goals:

  • 不切換至 HttpOnly Cookie(需要 HTTPS、CSRF token、OAuth 重構,成本過高)
  • 不修改既有 token 的儲存位置(localStorage → sessionStorage 留 P1
  • 不實作 token refresh / silent renew 機制(留 P1
  • 不清除資料庫中現存的永不過期 token(expires_at = null 的舊記錄接受自然淘汰;prune-expired 只刪 expires_at 已逾期的記錄,兩者不衝突)

Decisions

1. Token 過期時間設為 7 天

選擇sanctum.expiration = 10080(分鐘)

原因:7 天符合「使用者每週至少登入一次」的合理假設,過短(如 1 天)會顯著提高重新登入頻率影響 UX,過長則縮減不了多少攻擊視窗。

替代方案考慮

  • 1 天:洩漏視窗最小,但 UX 衝擊大
  • 30 天:常見電商慣例,但本平台含金流元素不適用
  • 無限:現狀,排除

副作用sanctum.expiration 只影響新建立的 token,既有 personal_access_tokensexpires_at = null 的記錄不受影響。可接受——舊 token 在使用者下次登入後自然被新 token 取代。

清理機制:啟用 expiration 後過期 token 不會自動從資料庫刪除,需搭配 sanctum:prune-expired --hours=168 排程(routes/console.php)每日清理,避免 personal_access_tokens 表持續膨脹。


2. Google OAuth redirect 改用 URL Fragment

選擇redirect(config('app.frontend_url') . '/auth/callback#token=' . $token)

原因

  • URL fragment# 後的部分)依 HTTP 規範不會被瀏覽器送到 server,因此不會出現在 Nginx/server log
  • Fragment 不會包含在 Referer header 中
  • Fragment 不會出現在瀏覽器歷史的完整記錄中(部分瀏覽器歷史仍記錄 fragment,但不在伺服器端)
  • 前端改動僅一行:route.query.tokennew URLSearchParams(window.location.hash.substring(1)).get('token')

替代方案考慮

  • 後端 POST 回 tokenOAuth provider 只能 redirect,無法直接 POST
  • server-side session 中繼:後端 redirect 到 /auth/callback?code=<random> 再由前端換 token——更安全但需要 server-side session store,架構複雜度提高
  • 保持 query string:現狀,洩漏路徑未消除,排除

選擇 fragment 而非 server-side session 的原因fragment 方案完全消除 server log 洩漏問題,且改動最小,對此平台規模足夠。


3. Rate Limiting 使用 Laravel 內建 throttle middleware

選擇

  • Member / Provider 登入:throttle:5,1
  • Admin 登入:throttle:3,1(帳號權限較高,適當收緊)

原因

  • Laravel 11 throttle middleware 基於 IP,不需要額外套件
  • 觸發時自動回傳 HTTP 429 並帶 Retry-After header
  • 5次/分鐘對正常使用者幾乎無感,但讓暴力破解的時間成本從秒級提高到分鐘級

替代方案考慮

  • throttle:10,1:太寬鬆,對所有角色排除
  • throttle:3,1 for member/provider:可能誤傷快速填錯密碼的正常使用者,故 member/provider 維持 5 次;admin 因帳號影響範圍大,3 次的誤傷成本可接受
  • Progressive delay(每次失敗增加等待時間):需要額外實作,P0 不引入複雜度

僅套用登入端點:register 端點不加(新帳號建立的攻擊面不同,留 P1)

Risks / Trade-offs

  • 既有 token 不過期expiration 改動不回溯,需接受現有洩漏的舊 token 持續有效直到使用者重新登入。風險可接受,因為:(1) 此為首次設定,無歷史洩漏事件;(2) 使用者正常使用就會刷新。
  • personal_access_tokens 表膨脹:啟用 expiration 後過期記錄不自動清除,需排程 sanctum:prune-expired → 已列入 tasks。
  • env() 在 config cache 下失效SocialAuthController 原本使用 env('FRONTEND_URL'),執行 php artisan config:cache 後(production 必做)env() 回傳 null,導致 redirect 到錯誤 URL。此 change 一併修正為 config('app.frontend_url')config/app.php 已有此鍵)。
  • fragment 在部分瀏覽器歷史仍可見Chrome DevTools Application → Session History 仍記錄完整 URL 含 fragment,但這是本地攻擊面(攻擊者需存取使用者設備),遠低於 server log 洩漏的遠端攻擊面。
  • IP-based throttle 可被繞過:攻擊者可換 IP 或使用代理。此方案擋的是基本掃描攻擊,不是針對性攻擊。P1 可加 account-level lockout。

Migration Plan

  1. 套用 config/sanctum.php 修改,重啟 PHP-FPM(或 Docker container
  2. routes/console.php 新增 Schedule::command('sanctum:prune-expired --hours=168')->daily();(無需重啟,下次排程週期生效)
  3. 套用 routes/api.php 修改,無需重啟(路由快取需清除:php artisan route:clear
  4. 套用後端 SocialAuthController.php 修改
  5. 套用前端 AuthCallbackView.vue 修改,重新 build frontendnpm run build
  6. 驗證:手動測試 Google OAuth 登入流程、登入頻率限制觸發、手動執行 php artisan sanctum:prune-expired --hours=168 確認排程指令可正常執行

Rollback:各項變更相互獨立,可逐項還原。Token 過期設定還原後,新建 token 重新改為不過期(舊的已過期的 token 無法恢復)。

Open Questions

  • Coach 登入端點 /api/provider/login 是否也需要 rate limiting(已包含在此 change 中,與 member 同風險級別,統一採用 throttle:5,1
  • Admin 登入是否需要更嚴格的限制?決策:改為 throttle:3,1,在此 change 實作。 管理員帳號一旦被暴力破解,影響範圍遠大於一般使用者(可存取所有用戶資料、審核功能),更嚴格的限制可接受,正常管理員操作不太可能在 1 分鐘內連續嘗試 3 次以上。
  • 是否需要對 Google OAuth callback 加入 state parameter 驗證(CSRF for OAuth)?確認留 P1。 風險評估:stateless() 模式下缺少 state 驗證,理論上存在 CSRF-for-OAuth 攻擊面(攻擊者偽造 callback 讓受害者綁定攻擊者的 Google 帳號)。緩解因素:(1) Google OAuth 本身要求使用者主動授權,不能靜默觸發;(2) 攻擊者需控制受害者瀏覽器的 callback 請求,難度高;(3) 平台目前無高價值資產(金流未上線)。P1 實作時加入 state parameter 並在 session 中驗證。