A04: 不安全的設計(Insecure Design)
這類漏洞指的是系統 設計本身 就存在安全風險,即使程式碼沒有立即的漏洞,攻擊者仍然可以利用設計缺陷來進行攻擊。
密碼重置功能的設計缺陷
許多網站允許用戶透過 "忘記密碼" 來重置密碼。如果這個功能設計不安全,攻擊者可能會透過 暴力猜測、簡單的驗證機制 或 邏輯漏洞 來重置目標帳戶的密碼。
❌ 有漏洞的密碼重置
這是一個不安全的 密碼重置系統,它允許用戶輸入電子郵件來請求密碼重置:
package main
import (
"database/sql"
"fmt"
"log"
"math/rand"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 建立 users 資料表
createTable := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
// 插入測試帳號
db.Exec("INSERT INTO users (email, password) VALUES ('victim@example.com', 'oldpassword')")
// 設定密碼重置功能
http.HandleFunc("/reset-password", handlePasswordReset)
fmt.Println("伺服器啟動於 http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func handlePasswordReset(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
email := r.FormValue("email")
// 產生簡單的 6 位數重置碼(不安全!)
rand.Seed(time.Now().UnixNano())
resetCode := fmt.Sprintf("%06d", rand.Intn(1000000))
// ✅ 直接顯示重置碼(重大安全漏洞!)
fmt.Fprintf(w, "你的密碼重置碼是:%s", resetCode)
// 更新資料庫
_, err := db.Exec("UPDATE users SET password=? WHERE email=?", resetCode, email)
if err != nil {
http.Error(w, "重置失敗", http.StatusInternalServerError)
return
}
}
❌ 漏洞分析
- 密碼重置碼是隨機 6 位數
- 攻擊者可以透過暴力破解 (最多 100 萬次請求),然後成功重置密碼。
- 密碼重置碼直接顯示在回應
- 這 代表 攻擊者只要提交電子郵件,就能拿到密碼重置碼,根本不需要取得目標的 Email!
- 直接用重置碼作為新密碼
- 新密碼過於簡單,這使得攻擊者可以直接嘗試 000000 ~ 999999 登入。
修補設計
✅ 安全設計
- 使用安全的密碼重置流程
- 生成強隨機 Token
- Token 存到資料庫,並加上過期時間
- 發送 Token 到用戶 Email
- 用戶輸入 Token 時要與資料庫比對
- 通過驗證後才允許用戶設定新密碼
- 新密碼需加密儲存
總結
🛠️ 修補後的安全的版本
package main
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"log"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlite3", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 建立 users 資料表
createTable := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
reset_token TEXT,
token_expiry DATETIME
);`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
// 插入測試帳 號
db.Exec("INSERT INTO users (email, password) VALUES ('victim@example.com', ?)", hashPassword("oldpassword"))
// 設定密碼重置功能
http.HandleFunc("/request-reset", handleRequestReset)
http.HandleFunc("/reset-password", handlePasswordReset)
fmt.Println("伺服器啟動於 http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
// 產生安全的隨機 Token
func generateSecureToken() (string, error) {
bytes := make([]byte, 32)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// 將密碼進行加密
func hashPassword(password string) string {
hashed, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hashed)
}
// 處理密碼重置請求
func handleRequestReset(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
email := r.FormValue("email")
// 產生 Token
resetToken, err := generateSecureToken()
if err != nil {
http.Error(w, "伺服器錯誤", http.StatusInternalServerError)
return
}
// 設定 Token 過期時間(例如 15 分鐘)
expiry := time.Now().Add(15 * time.Minute)
// 存入資料庫
_, err = db.Exec("UPDATE users SET reset_token=?, token_expiry=? WHERE email=?", resetToken, expiry, email)
if err != nil {
http.Error(w, "重置失敗", http.StatusInternalServerError)
return
}
// ✅ 模擬發送 Email(實際應該寄 Email)
fmt.Fprintf(w, "重置連結: http://localhost:8080/reset-password?token=%s", resetToken)
}
// 處理密碼重置
func handlePasswordReset(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
token := r.FormValue("token")
newPassword := r.FormValue("new_password")
// 從資料庫查找 Token
var email string
var expiry time.Time
err := db.QueryRow("SELECT email, token_expiry FROM users WHERE reset_token=?", token).Scan(&email, &expiry)
if err != nil {
http.Error(w, "無效的重置請求", http.StatusUnauthorized)
return
}
// 檢查 Token 是否過期
if time.Now().After(expiry) {
http.Error(w, "重置碼已過期", http.StatusUnauthorized)
return
}
// ✅ 加密新密碼
hashedPassword := hashPassword(newPassword)
// ✅ 更新密碼並清除 Token
_, err = db.Exec("UPDATE users SET password=?, reset_token=NULL, token_expiry=NULL WHERE email=?", hashedPassword, email)
if err != nil {
http.Error(w, "重置失敗", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "密碼重置成功!")
}
🛠️ 修補後的安全性
問題 | 修正方式 |
---|---|
弱密碼重置碼 | 使用 32 位元隨機 Token |
重置碼直接顯示 | Token 透過 Email 發送 |
密碼過於簡單 | 新密碼儲存前雜湊 |
Token 沒有過期時間 | 設定 15 分鐘時效 |