Skip to content

feat(sec-qa): Webhook 權限、共享限流與前端 Error Boundary - #2

Merged
frobel0520 merged 10 commits into
mainfrom
feature/sec-qa-hardening
Sep 21, 2026
Merged

frobel0520 merged 10 commits into
mainfrom
feature/sec-qa-hardening

Conversation

@frobel0520

@frobel0520 frobel0520 commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Draft PR:先用來觸發 CI(ci.yml 只在 push 到 main 或 pull request 時執行)。

內容

  • SEC-QA-01:webhook 管理與事件查詢需登入且在 dify_access 名單;目的地限 WEBHOOK_ALLOWED_URLS 精確 HTTPS 網址,禁止 redirect
  • SEC-QA-02:Dify user 取自驗證後的 Supabase UUID;Postgres 原子計數限流,超額 429 + Retry-After
  • SEC-QA-03:React Error Boundary + Playwright smoke test
  • CI 新增 frontend 與 deno-api(Postgres 17 service)job

修正的 bug

consume_api_rate_limit 原本永遠回傳 allowed。已修正,並以 rate_limit_test.ts 在真實 Postgres 上重現與驗證。

待辦

  • CI 的 deno-api(首次執行 deno check index.ts)與 frontend smoke 通過(b031514 起)
  • 錯誤 HTTP 方法(如 GET /ask)回 503,應為 404/405(92a7fa8)
  • 前端 422 驗證錯誤不再顯示原因(71f2ad3)
  • 部署文件補上 WEBHOOK_ALLOWED_URLS,並註明 migration 需先於 Edge Function 上線(a031f17)

線上後端已停用(2026-09-21)

原 Supabase 專案已移除,這個 PR 的 migration 不會套用到任何線上資料庫。處理方式:

  • 41ae488:沒有 API 網址的 Pages build 顯示「線上 API 已停用」並停止自動呼叫 API;README 註明目前未部署
  • 已刪除指向舊專案的 SUPABASE_* repository variables,並停用 Deploy Supabase Edge Function workflow
  • 日後重建:依 deploy/github-supabase-deploy.md 建專案、跑 migration、設定 variables 並重新啟用 workflow,停用提示會自動消失

🤖 Generated with Claude Code

- SEC-QA-01:webhook 管理與事件查詢改為需登入且在 dify_access 名單;目的地限 WEBHOOK_ALLOWED_URLS 的精確 HTTPS 網址,每次送出重新檢查、禁止 redirect
- SEC-QA-02:Dify user 改用驗證後的 Supabase UUID;新增 Postgres 原子計數的全域/每帳號限流(migration 20260909000000),超額回 429 + Retry-After,限流不可用時回 503
- SEC-QA-03:React Error Boundary、401/403/429 錯誤訊息、Playwright smoke test
- CI:新增 frontend(build + smoke)與 deno-api(Postgres 17 service)job
- docs:SA/SD 更新權限矩陣,新增 ADR-006、ADR-007

修正 bug:consume_api_rate_limit 在計數達上限後停止累加,但判斷式是 request_count <= p_limit,導致永遠放行。改成超額時計數推到 p_limit + 1 後停住(check 約束同步放寬到 1000001),並新增 rate_limit_test.ts 以真實 Postgres 重現:上限 3、呼叫 5 次,修正前全部放行,修正後第 4、5 次拒絕。

驗證:本機 Postgres 18 已確認重現與修正;npm run build 通過;Deno 測試與 Playwright smoke 待 CI 執行。待辦:錯誤 HTTP 方法回 503、前端 422 不顯示原因、WEBHOOK_ALLOWED_URLS 未寫入部署文件。
GitHub Actions 的 job 層級 if 不支援 hashFiles(),整個 ci.yml 因此被判定為無效,PR #2 的 CI 完全沒有執行(錯誤:Unrecognized function: 'hashFiles',Line 47)。現在已有 rate_limit_test.ts,這個條件不再需要,直接移除。

驗證:本機無法執行 Actions,push 後由 PR #2 的 CI 確認。
askDify 在同一個 scope 宣告了兩次 const result,deno check 報 TS2451;在執行期這也是 SyntaxError,整個 Edge Function 會載入失敗、所有路由都無法使用。改為直接解構 executeDifyRequest 的 question 與 data。

驗證:esbuild 解析修正前報「result has already been declared」、修正後通過;完整型別檢查由 PR #2 的 deno-api job 確認。
vite.smoke.config.js 沒有設定 base,smoke build 產出的資源路徑是 /assets/...,但 serve-dist.mjs 只服務 /smoke/ 底下的路徑,JS 與 CSS 全部 404、頁面空白,三個 smoke test 都找不到標題。改成 base: "./",與正式 build(vite build --base ./)一致。

驗證:重新 build 後 index.html 與 tests/error-boundary.html 的資源都是相對路徑,/smoke/ 底下 4 個資源皆回 200;瀏覽器實測主頁標題與導覽列、Error Boundary 標題與重新載入按鈕都有顯示,錯誤細節未外露。Playwright 本身由 PR #2 的 frontend job 確認。
同一則錯誤訊息會同時出現在全域狀態列與 WebHook 結果框,getByText 對到兩個元素,Playwright strict mode 判定失敗(CI 3 passed / 1 failed)。改為直接斷言 #webhookResult 的完整文字,同時更精準驗證是該操作的結果。

驗證:瀏覽器以 smoke build 模擬 401、403、429,#webhookResult 文字與預期完全相符、三次請求都帶 Bearer token、後端 detail 未外露;Playwright 由 PR #2 的 frontend job 確認。
- rateLimitRouteKey 移到 security.ts;組合不在 consume_api_rate_limit 允許清單時退回 <METHOD>:other,路由層才能照常回 404/405
- 新增 RATE_LIMIT_ROUTE_KEYS 與 security_test.ts:檢查集合與 migration 清單一致,且 6 種方法 × 13 種路由都產生資料庫接受的 key

修正 bug:GET /ask、PUT /webhooks、PATCH /notes/1 等 78 種組合中有 35 種產生 SQL 允許清單外的 key,RPC 丟錯後被當成限流不可用回 503。

驗證:esbuild 轉譯後以 Node 跑同一組檢查,修正前 35 種被拒、修正後 0;Deno 測試由 PR #2 CI 執行。
- 422 且 detail 為 200 字內字串時顯示原因;結構化 detail(如 FastAPI 陣列)只顯示通用訊息
- 新增 413 訊息;401/403/429 與其他狀態維持不揭露後端 detail
- smoke test 新增 422 兩種情境

驗證:npm run build 通過;瀏覽器以 smoke build 模擬 422 字串、422 陣列、413,#noteResult 皆符合預期、陣列內容未外露;Playwright 由 PR #2 CI 執行。
- 兩份部署文件加上一律執行 20260909000000_add_edge_rate_limit.sql(schema.sql 未包含)
- 補上 WEBHOOK_ALLOWED_URLS 的格式與留空時的行為
- 註明 push 到 main 會自動部署 Edge Function 但不跑 migration,含 migration 的 PR 進 main 前要先手動執行

驗證:文件描述對照 security.ts 的 URL 規則與 supabase-functions.yml 的觸發條件。
- 沒有設定 API 網址的 Pages build(非本機)視為線上 API 已停用:頁首、hero、側欄顯示停用說明與本機啟動方式,不再自動打 /health、/notes
- README 開頭與「上線部署」註明 Supabase 專案已移除、目前未部署,Supabase 程式與部署文件保留供重建
- 重新設定 SUPABASE_* variables 後停用提示會自動消失,不需改程式

驗證:npm run build 通過;以 127.0.0.2 模擬無 API 網址的 Pages build,停用提示只出現一次、hero 不再要求確認 Edge Function 部署、頁面載入零次 API 請求;本機與 smoke build 行為不變,Playwright 由 PR #2 CI 執行。
@frobel0520
frobel0520 marked this pull request as ready for review September 21, 2026 14:09
@frobel0520
frobel0520 merged commit 9f2bd0c into main Sep 21, 2026
3 checks passed
frobel0520 added a commit that referenced this pull request Sep 21, 2026
GitHub Actions 的 job 層級 if 不支援 hashFiles(),整個 ci.yml 因此被判定為無效,PR #2 的 CI 完全沒有執行(錯誤:Unrecognized function: 'hashFiles',Line 47)。現在已有 rate_limit_test.ts,這個條件不再需要,直接移除。

驗證:本機無法執行 Actions,push 後由 PR #2 的 CI 確認。
frobel0520 added a commit that referenced this pull request Sep 21, 2026
askDify 在同一個 scope 宣告了兩次 const result,deno check 報 TS2451;在執行期這也是 SyntaxError,整個 Edge Function 會載入失敗、所有路由都無法使用。改為直接解構 executeDifyRequest 的 question 與 data。

驗證:esbuild 解析修正前報「result has already been declared」、修正後通過;完整型別檢查由 PR #2 的 deno-api job 確認。
frobel0520 added a commit that referenced this pull request Sep 21, 2026
vite.smoke.config.js 沒有設定 base,smoke build 產出的資源路徑是 /assets/...,但 serve-dist.mjs 只服務 /smoke/ 底下的路徑,JS 與 CSS 全部 404、頁面空白,三個 smoke test 都找不到標題。改成 base: "./",與正式 build(vite build --base ./)一致。

驗證:重新 build 後 index.html 與 tests/error-boundary.html 的資源都是相對路徑,/smoke/ 底下 4 個資源皆回 200;瀏覽器實測主頁標題與導覽列、Error Boundary 標題與重新載入按鈕都有顯示,錯誤細節未外露。Playwright 本身由 PR #2 的 frontend job 確認。
frobel0520 added a commit that referenced this pull request Sep 21, 2026
同一則錯誤訊息會同時出現在全域狀態列與 WebHook 結果框,getByText 對到兩個元素,Playwright strict mode 判定失敗(CI 3 passed / 1 failed)。改為直接斷言 #webhookResult 的完整文字,同時更精準驗證是該操作的結果。

驗證:瀏覽器以 smoke build 模擬 401、403、429,#webhookResult 文字與預期完全相符、三次請求都帶 Bearer token、後端 detail 未外露;Playwright 由 PR #2 的 frontend job 確認。
frobel0520 added a commit that referenced this pull request Sep 21, 2026
- rateLimitRouteKey 移到 security.ts;組合不在 consume_api_rate_limit 允許清單時退回 <METHOD>:other,路由層才能照常回 404/405
- 新增 RATE_LIMIT_ROUTE_KEYS 與 security_test.ts:檢查集合與 migration 清單一致,且 6 種方法 × 13 種路由都產生資料庫接受的 key

修正 bug:GET /ask、PUT /webhooks、PATCH /notes/1 等 78 種組合中有 35 種產生 SQL 允許清單外的 key,RPC 丟錯後被當成限流不可用回 503。

驗證:esbuild 轉譯後以 Node 跑同一組檢查,修正前 35 種被拒、修正後 0;Deno 測試由 PR #2 CI 執行。
frobel0520 added a commit that referenced this pull request Sep 21, 2026
- 422 且 detail 為 200 字內字串時顯示原因;結構化 detail(如 FastAPI 陣列)只顯示通用訊息
- 新增 413 訊息;401/403/429 與其他狀態維持不揭露後端 detail
- smoke test 新增 422 兩種情境

驗證:npm run build 通過;瀏覽器以 smoke build 模擬 422 字串、422 陣列、413,#noteResult 皆符合預期、陣列內容未外露;Playwright 由 PR #2 CI 執行。
@frobel0520
frobel0520 deleted the feature/sec-qa-hardening branch September 21, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant