最近用 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 系統,這個層級的防護已經足夠。但如果是多用戶或商業系統,還需要更嚴格的審計和滲透測試。