Skip to content

fix(security): 伺服器端強制 Turnstile 驗證 + KV 批量清理工具(修復大量刪除逾時) - #4

Merged
su-nz merged 1 commit into
mainfrom
claude/peaceful-edison-lcAFA
Jun 3, 2026
Merged

su-nz merged 1 commit into
mainfrom
claude/peaceful-edison-lcAFA

Conversation

@su-nz

@su-nz su-nz commented Jun 3, 2026

Copy link
Copy Markdown
Owner

背景

有人繞過前端、直接呼叫公開建立 API 大量灌入短網址,導致 KV 內累積上萬筆無用資料。

根因

functions/api/public-create.js 只在前端跑 Cloudflare Turnstile,伺服器端完全沒驗證 —— 前端把 turnstileToken 放進 body,後端收到後直接忽略。攻擊者只要直接 POST /api/public-create(校內 IP 即可過白名單)就能無限建立短網址。functions/lib/validation.js 早已寫好 verifyTurnstile()SPEC.md 第 168 行也明文要求驗證 Turnstile,只是從未被實作。

變更內容

🔒 安全性修復(api/public-create.js

  • 伺服器端強制呼叫 verifyTurnstile(turnstileToken, env.TURNSTILE_SECRET, ip)
  • 驗證排在所有 KV 寫入之前,失敗的攻擊請求不再污染資料庫或灌爆速率限制計數。
  • Fail-closed:缺 token 或未設定 secret 一律拒絕;僅 DEV_MODE=true 可略過(本機測試)。
  • 驗證失敗時發送 Discord「存取被拒」通知。

🧹 KV 批量清理工具(新頁面 /admin/cleanup

  • 逐頁載入 KV 內全部短網址,可依建立時間區間 / 建立者 IP / 目標網址或短碼 / 0 點擊篩選,「全選符合篩選」後批量刪除。
  • 為效能畫面最多渲染 1000 列,但選取與刪除一律套用到全部符合篩選的資料。
  • 管理後台新增「🧹 批量清理」入口。

⚡ 大量刪除效能修復(修 524 逾時 / 卡頓)

先前批量刪除為「100 筆/批、每筆 3 個 KV delete、逐筆 await」,一次刪上萬筆時每個請求 300 個序列子請求 → 超出 Cloudflare ~100s 上限 → 524;免費方案 50 子請求上限下更直接失敗。

  • 後端 admin DELETE 的 silent 模式改為 Promise.all 平行刪除,請求數秒內完成。
  • 移除 disabled:${id} 的刪除——該鍵在本專案從未被寫入(停用狀態存於 link metadata),可省下 1/3 子請求。
  • 前端切成 200 筆/批、並行度 3,批次失敗時自動對半拆分重試以適應不同方案的子請求上限;改用 Set 記錄成功/失敗,消除 O(n²) 卡頓,失敗項目保留可重試。

對既有資料的影響

。此 PR 不會自動讀寫、遷移或修改任何現有 KV 資料:

  • 轉址路徑 [id].js 完全未改,所有現有短網址照常運作。
  • 清理頁面載入資料為純讀取(list + getWithMetadata),刪除只在管理員主動勾選後才發生。
  • Turnstile 驗證只影響「未來新建」請求。

⚠️ 部署前必做

Production 必須設定環境變數 TURNSTILE_SECRET(對應前端的 TURNSTILE_SITE_KEY)。因改為 fail-closed,若未設定,新建短網址功能會被擋下(但不影響既有短網址轉址)。

https://claude.ai/code/session_01Y6XCMnr6W2tWg5ynwq1Yst


Generated by Claude Code

先前批量刪除以「100 筆/批、每筆 3 個 KV delete、逐筆 await」的方式
進行,一次刪除上萬筆時:
- 每個請求 300 個序列子請求 → 超出 Cloudflare ~100s 上限 → 524。
- 免費方案 50 子請求/請求上限下更是直接失敗。
- 前端用陣列 includes 比對失敗清單,造成 O(n^2) 卡頓。

後端 (admin DELETE silent 模式):
- 改為 Promise.all 平行刪除,請求數秒內完成,避免 524。
- 移除 disabled:${id} 的刪除——該鍵在本專案從未被寫入(停用狀態存於
  link metadata),可省下 1/3 子請求。
- 單次上限放寬至 200 筆。

前端 (/admin/cleanup):
- 切成 200 筆/批,並以並行度 3 處理,加速上萬筆刪除。
- 批次失敗時自動對半拆分重試,自動適應不同方案的子請求上限。
- 改用 Set 記錄成功/失敗,消除 O(n^2) 卡頓;失敗項目保留可重試。
- 進度顯示成功/失敗即時數量。
Copilot AI review requested due to automatic review settings June 3, 2026 08:42
@su-nz
su-nz merged commit ff9c6eb into main Jun 3, 2026
2 checks passed
@su-nz
su-nz deleted the claude/peaceful-edison-lcAFA branch June 3, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the admin-side bulk deletion workflow for short links stored in LINKS_KV, focusing on making large cleanup operations faster and less likely to time out by adjusting server-side delete behavior and optimizing the /admin/cleanup UI’s batching strategy.

Changes:

  • Updates admin DELETE handling to support larger batches (up to 200 IDs) and perform silent-mode deletions in parallel with a single summary Discord notification.
  • Optimizes the /admin/cleanup page’s bulk delete flow with bounded concurrency, automatic batch splitting on failure, and Set-based bookkeeping to avoid UI slowdowns.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
functions/admin/index.js Adjusts admin delete API limits and implements parallelized silent bulk deletion with summary notification.
functions/admin/cleanup.js Improves bulk delete client logic via chunking, concurrency control, and retry-by-splitting to handle large datasets efficiently.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread functions/admin/index.js
Comment on lines +199 to +202
// 靜默批量模式(清理用):平行刪除、不逐筆讀取或發送 Discord 通知,
// 避免一次刪除大量項目時拖慢請求、轟炸 webhook 或超出子請求上限。
// 注意:本專案從未寫入 disabled:${id} 鍵(停用狀態存於 link metadata),
// 因此只需刪 link: 與 stats:,可省下 1/3 子請求。
Comment on lines +432 to +434
// 刪除單一批次;若請求失敗(例如超出子請求上限),自動對半拆分重試,
// 直到批次小於 MIN_BATCH 才判定失敗。回傳成功刪除的 id 陣列。
async function deleteOneBatch(batch, okSet, failedSet, onProgress) {
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.

3 participants