Skip to content

fix(security): 伺服器端強制 Turnstile 驗證 + 新增 KV 批量清理工具 - #3

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,只是從未被實作。

變更內容

🔒 安全性修復

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

🧹 KV 批量清理工具

  • 新增 /admin/cleanup 頁面:逐頁載入 KV 內全部短網址,可依建立時間區間 / 建立者 IP / 目標網址或短碼 / 0 點擊篩選,「全選符合篩選」後批量刪除(每批 100 筆並顯示進度)。
  • 為效能畫面最多渲染 1000 列,但選取與刪除一律套用到全部符合篩選的資料。
  • admin DELETE 新增 silent 批量模式:直接刪除、不逐筆發 Discord 通知(改為一則彙總),並一併清除 disabled: 鍵,單次上限 100 筆。
  • 管理後台新增「🧹 批量清理」入口。

對既有資料的影響

。此 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

公開建立短網址 API (/api/public-create) 先前只在前端做 Turnstile
驗證,攻擊者可繞過前端直接呼叫 API 灌入大量短網址。

安全性修復:
- public-create 在伺服器端強制驗證 Turnstile(沿用既有的
  verifyTurnstile),驗證在任何 KV 寫入之前執行,避免攻擊流量
  污染資料庫或灌爆速率限制計數;缺少 token / 未設定 secret 一律
  拒絕(fail-closed),DEV_MODE 可略過以利本機測試。
- 驗證失敗時發送 Discord 存取被拒通知。

KV 清理工具:
- 新增 /admin/cleanup 頁面,逐頁載入 KV 內全部短網址,可依建立
  時間、建立者 IP、目標網址/短碼、0 點擊篩選,全選後批量刪除。
- /admin DELETE 新增 silent 批量模式:直接刪除、不逐筆發 Discord
  通知(改為一則彙總),並一併清除 disabled: 鍵,單次上限 100 筆。
- 管理後台新增「批量清理」入口。
Copilot AI review requested due to automatic review settings June 3, 2026 08:19
@su-nz
su-nz merged commit c2f9d17 into main Jun 3, 2026
2 checks passed
@su-nz
su-nz deleted the claude/peaceful-edison-lcAFA branch June 3, 2026 08:20

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 addresses abuse of the public short-link creation endpoint by enforcing server-side Cloudflare Turnstile verification, and adds an admin-facing bulk cleanup UI to safely review/filter/delete large volumes of KV-stored links.

Changes:

  • Enforce Turnstile validation in POST /api/public-create before any KV writes (incl. rate-limit counters), with fail-closed behavior and Discord notification on denial.
  • Add /admin/cleanup page that paginates through KV link: keys, supports filtering and “select all filtered”, and performs batched deletions via DELETE /admin using a new silent bulk mode.
  • Extend admin delete behavior to support silent mode and add an “🧹 批量清理” entry point in the admin UI.

Reviewed changes

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

File Description
functions/api/public-create.js Adds server-side Turnstile verification before rate limiting / KV writes and notifies Discord on denial.
functions/admin/index.js Adds silent bulk delete mode and UI link to the new cleanup page; introduces per-request deletion cap.
functions/admin/cleanup.js New admin cleanup UI + JSON data endpoint to enumerate KV links and batch delete selected items.

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

Comment on lines +56 to +66
const cookie = request.headers.get('Cookie') || '';
const sessionMatch = cookie.match(/admin_session=([^;]+)/);
if (!sessionMatch) {
return { ok: false, response: Response.redirect(new URL('/admin', request.url).href, 302) };
}

const sessionKey = sessionMatch[1];
const sessionData = await env.LINKS_KV.get(`session:${sessionKey}`, { type: 'json' });
if (!sessionData || Date.now() >= sessionData.expiresAt) {
return { ok: false, response: Response.redirect(new URL('/admin', request.url).href, 302) };
}
Comment thread functions/admin/index.js
Comment on lines +193 to +196
// 單一請求最多刪除 100 筆,避免超出 Workers 子請求上限
if (ids.length > 100) {
return createErrorResponse('Too many IDs. Maximum 100 per request.', 'TOO_MANY_IDS', 400);
}
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