/

打造 Blog 系統時,我修復了哪些資安漏洞

最近用 Next.js + Turso + ByteMD 建了一套個人 Blog 系統。在完成基本功能後,我系統性地審視了整個系統的資安設計,發現了幾個需要修復的漏洞。這篇文章記錄修復的過程與思路。


高風險漏洞

1. 登入端點無頻率限制(Brute Force)

問題

/api/auth/login 沒有任何限制,攻擊者可以用自動化工具無限次嘗試密碼,直到猜中為止。

修法

在 Turso 資料庫新增 login_attempts 資料表,記錄每個 IP 的失敗次數。每次登入請求先查詢:

SELECT COUNT(*) FROM login_attempts
WHERE ip = ? AND attempted_at > datetime('now', '-15 minutes')

若超過 5 次就回傳 429 Too Many Requests,登入成功後清除該 IP 的記錄。


2. JWT Secret 無強制設定

問題

lib/auth.ts 有這樣一行:

process.env.JWT_SECRET || "default-secret-change-me"

如果部署時忘記設定環境變數,系統會默默使用這個弱密鑰簽發 token,攻擊者只要知道這行 fallback 就能偽造任何人的 JWT。

修法

在 production 環境直接拋出錯誤,強迫必須設定:

if (!process.env.JWT_SECRET && process.env.NODE_ENV === "production") {
  throw new Error("JWT_SECRET environment variable must be set in production");
}

中風險漏洞

3. CSRF 跨站請求偽造

問題

攻擊者可以建立一個惡意網頁,讓已登入的管理員在不知情的情況下觸發刪除文章等操作。

修法

在 Next.js middleware 中,對所有 POST/PUT/DELETE 請求驗證 Origin header 必須與 Host 一致:

function isValidOrigin(request: NextRequest): boolean {
  const origin = request.headers.get("origin");
  const host = request.headers.get("host");
  if (!origin) return true;
  if (host && origin.endsWith(host)) return true;
  return false;
}

補充:由於登入 cookie 設定了 SameSite=lax,大多數 CSRF 攻擊已被瀏覽器層擋下,這是額外的防禦層。


4. 登出後 JWT Token 仍然有效

問題

登出只是清除 cookie,但 JWT token 本身在到期前(7 天)依然合法。如果 token 被竊取,無法撤銷。

修法

新增 revoked_tokens 資料表,儲存 token 的 SHA-256 hash:

CREATE TABLE revoked_tokens (
  token_hash TEXT PRIMARY KEY,
  revoked_at TEXT DEFAULT (datetime('now'))
);

登出時呼叫 revokeToken(token),每次請求進入 middleware 前先查詢黑名單。超過 8 天的記錄自動清理,避免資料表無限增長。


5. Slug 衝突導致 500 錯誤洩漏內部資訊

問題

建立重複 slug 的文章時,SQLite 的 UNIQUE 約束拋出的原始錯誤訊息直接回傳給前端,可能洩漏資料庫結構資訊。

修法

在 API route 的 catch 區塊中判斷錯誤類型:

if (err.message.toLowerCase().includes("unique")) {
  return NextResponse.json(
    { error: "此 Slug 已被使用,請更換其他名稱" },
    { status: 409 }
  );
}
return NextResponse.json({ error: "建立文章失敗" }, { status: 500 });

結語

安全不是一次性的工作,而是持續的過程。這次修復的是最基礎的防線,後續還有值得改進的地方,例如 CSP headers、管理員帳號變更密碼功能等。

作為個人 Blog 系統,這個層級的防護已經足夠。但如果是多用戶或商業系統,還需要更嚴格的審計和滲透測試。

分享