跳至主要内容

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
}
}

❌ 漏洞分析

  1. 密碼重置碼是隨機 6 位數
    • 攻擊者可以透過暴力破解 (最多 100 萬次請求),然後成功重置密碼。
  2. 密碼重置碼直接顯示在回應
    • 這代表 攻擊者只要提交電子郵件,就能拿到密碼重置碼,根本不需要取得目標的 Email!
  3. 直接用重置碼作為新密碼
    • 新密碼過於簡單,這使得攻擊者可以直接嘗試 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 分鐘時效