今天是鐵人賽第十一天。
昨天與 AI 同事建構使用者管理端點後,我注意到 AI 同事是使用流水號作為 USER ID。
我覺得這樣可能會有問題,因此請 AI 同事分析一下潛在風險。
AI 同事分析後確認的確存在資安疑慮,可能會有以下幾個問題:
他建議我改用 UUID 取代流水號作為 ID。
不過我記得 UUID 也有其問題,例如可能發生 碰撞(Collision),而這會與資料庫的唯一主鍵衝突。
我請 AI 同事思考碰撞的問題,他表示發生碰撞的機率比被雷打到還低,就算真的發生,資料庫也會拋出錯誤,應用程式只要重試插入新的 UUID 就能解決。
但他也提醒,如果沒有處理,碰撞發生時程式就會卡住。
最後,他建議以我們的專案來說,使用 UUIDv4 就足夠,不需要用到:
於是我請 AI 同事用 UUIDv4 取代原本的流水號 ID,並因為資料庫結構改變,請他生成一個刪除舊資料庫的腳本,以免插入新資料時出現問題。
結果又出現了老問題:
import uuid
,導致錯誤通常遇到這種範圍較大的修改,AI 同事很難一次做到位。
我的做法是讓他先建構雛形,執行後出現錯誤,再把錯誤訊息告訴他,讓他持續修改,直到所有錯誤被修正。
修改完成後,我請他處理可能的碰撞問題,並實作重試機制。
他一開始認為這是過度設計,因為機率極低,但我認為這種低機率錯誤才最麻煩,因為真的發生時難以重現,也不容易追蹤錯誤來源,除錯成本高。
我希望既然現在知道,就應該提前處理。
最後 AI 同事被我說服了 XD,承認這不是過度設計,而是對系統穩定性的投資。
在測試刪除資料庫並重新新增管理者帳號後,這次修改順利完成。
我請 AI 同事將修改內容更新到檔案並進行版本控制,結束今天的進度。
以下為今天的研究流程
首先開啟前的老樣子,請AI同事了解應用現況。
我給AI的指令:
@ai_context.md @README.md @軟體核心架構規劃.md @功能需求總覽.md 了解目前現況
了解後,因為我後來想了一下,我對USER ID
用流水號有點疑慮,想請AI同事思考一下這個部分。
我給AI的指令:
@agents/engineering/backend-architect.md 請幫我分析,user id用流水號有什麼資安疑慮,如果有,建議用什麼去代替,例如UUID之類?
AI同事思考後給我的回覆:
是的,使用流水號作為 user id 確實存在資安疑慮,主要體現在以下幾點:
- 枚舉攻擊 (Enumeration Attacks):
- 攻擊者可以輕易地透過遞增或遞減 user id 來猜測其他有效的使用者 ID。
- 這使得攻擊者能夠枚舉系統中的所有使用者帳戶,即使這些帳戶並未公開。
- 一旦使用者帳戶被枚舉,攻擊者就可以針對這些帳戶進行後續的攻擊,例如暴力破解密碼、釣魚攻擊或社會工程。
- 資訊洩露 (Information Leakage):
- 流水號 ID 可能會洩露系統的敏感資訊,例如:
- 系統中總共有多少使用者。
- 使用者註冊的趨勢(例如,如果 ID 突然跳躍,可能表示有大量新使用者註冊)。
- 特定時間段內的使用者增長速度。
- 這些資訊雖然看似無害,但在某些情況下可能被惡意利用。
- 可預測性 (Predictability):
- 由於 ID 是可預測的,攻擊者更容易構造惡意請求。
- 例如,在沒有嚴格授權檢查的情況下,攻擊者可以簡單地修改 URL 或 API 請求中的 user id 參數,嘗試存取或修改其他使用者的資料,這就是所謂的 不安全直接物件引用 (IDOR - Insecure Direct Object References) 漏洞。雖然授權檢查是主要防禦,但可預測的 ID 讓攻擊者更容易發現潛在的 IDOR 漏洞。
建議替代方案:UUID (Universally Unique Identifier)
作為後端架構師,我強烈建議使用 UUID (Universally Unique Identifier) 來替代流水號作為 user id。
UUID 的優點:
- 不可預測性:UUID 是一個 128 位元的數字,通常表示為 32 個十六進位數字,分為 5
組,由連字號分隔(例如:a1b2c3d4-e5f6-7890-1234-567890abcdef)。由於其巨大的空間(2^128
種可能性)和隨機性,幾乎不可能被猜測或枚舉,大大降低了枚舉攻擊的風險。- 分散式生成:UUID 可以在不依賴中央協調的情況下生成,這對於分散式系統或微服務架構非常有利。
- 全球唯一性:在實踐中,UUID 衝突的可能性極低,可以保證其在全球範圍內的唯一性。
UUID 的考量:- 儲存空間:UUID 比整數 ID 佔用更多的儲存空間(16 位元組 vs. 4-8 位元組)。
- 索引效能:由於 UUID 的隨機性,在資料庫中作為主鍵時,可能會導致索引碎片化,進而影響查詢效能。然而,對於大多數應用程式而言,這種效能影響通常在可接受範圍內。對於極大規模的應用,可以考慮使用時間有序的 UUID版本(如 UUIDv1 或 UUIDv7)來改善索引效能。
- 可讀性:UUID 不如整數 ID 直觀和易於人類閱讀。
看起來流水號的確可能造成一些問題,不過我記得用UUID這種隨機生成ID是有機會造成碰撞的,我請AI同事想一下如果碰撞該怎麼處理。
我給AI的指令:
但UUID也可能遇到碰撞問題,該怎麼處理碰撞
AI 同事給我的回饋:
✦ 您提出了一個非常好的問題
理論上,UUID 確實存在碰撞(Collision)的可能性,但對於大多數實際應用來說,這種可能性極低,幾乎可以忽略不計。作為後端架構師,我會這樣解釋並處理 UUID 碰撞問題:
1. UUID 碰撞的機率有多低?
我們主要討論的是 UUIDv4(基於隨機數生成),其包含 122 位元的隨機性(其餘位元用於版本與變體資訊)。
- 要達到 50% 的碰撞機率,需要生成約 2.7 × 10¹⁸ 個 UUID。
- 若 每秒生成 10 億個 UUID,達到 50% 機率需約 85 年。
- 即使地球上每人擁有 600 萬個 UUID,碰撞機率仍小於 十億分之一。
這個機率比您被閃電擊中、中樂透頭獎,或硬碟同時發生位元翻轉導致資料損壞還要低得多。
因此,在實際應用中,UUIDv4 的碰撞可視為不可能發生的事件。
2. 如何防範這種極低機率的碰撞?
雖然機率極低,但嚴謹的系統設計仍會有防護措施:
a. 資料庫唯一性約束(Database Unique Constraint)— 主要防線
- 將 UUID 作為主鍵或唯一索引時,資料庫會強制執行唯一性約束。
- 若應用程式生成重複 UUID 並插入,資料庫會拋出 唯一性約束錯誤(如 PostgreSQL 的
duplicate key value violates unique constraint
)。- 這是最堅固的保障,確保資料庫不會存在重複 UUID。
b. 應用程式層的重試機制(Retry Mechanism)
- 當資料庫因唯一性約束而拒絕插入時,應用程式應捕獲此錯誤。
- 捕獲後可生成新的 UUID 並重試插入操作。
- 由於碰撞機率極低,該重試機制幾乎不會被觸發,但可作為優雅的錯誤處理方式。
c. 監控(Monitoring)
- 可監控資料庫錯誤日誌,特別是唯一性約束違規的錯誤。
- 若頻繁出現,可能是 UUID 生成器隨機性不足,而非真正的碰撞。
3. 其他 UUID 版本
UUIDv1(基於時間與 MAC 位址)
- 碰撞機率更低,因為結合了時間戳與網卡 MAC 位址。
- 缺點:可能洩露生成時間與機器資訊,且在部分虛擬化環境下 MAC 位址可能不唯一。
UUIDv7(時間有序的 UUID)
- 新標準,結合 UUIDv4 的隨機性與 UUIDv1 的時間有序性。
- 開頭包含時間戳,可提升資料庫索引效能,同時保有隨機性以避免碰撞。
- 適合寫入量極大且對索引效能要求高的場景。
結論
對於 TaskWhisker 這樣的專案,使用 UUIDv4 作為 user id
是完全足夠且安全的。其碰撞機率在實際應用中可以被視為零。最關鍵的防範措施是在資料庫層面設定唯一性約束,這將確保即使在極端理論情況下發生碰撞,資料庫也會阻止重複資料的寫入,並觸發應用程式的重試機制。
根據AI同事的回答,我們用UUIDv4去取代id並有重試機制去處理碰撞就可以解決。
我給AI的指令:
好的,請依照你的建議使用UUIDv4去取代原本的流水號ID,目前資料庫都是測試資料直接清空就好不用轉移。並檢查我們的應用是否有應用程式層的重試機制。
這裡因為碰撞機率低,所以AI同事勸我不要過度設計,但我認為這種低機率發生的錯誤,會造成後續使用上的麻煩,假如真的發生了,光想重新觸發錯誤就是一個困難。因此我嘗試說服AI同事。
我給AI的指令:
我認為這不算是過度設計,而且這種低機率出現的錯誤,現在不處理,等以後出現光要找問題或重現都是困難的,希望這邊直接處理掉。
然後AI同事就被我說服了,AI同事直接說我說的對XD,這不是過度設計,而是對系統穩定性的投資。
他幫我把原本用到流水號的程式都改成uuid後,因為資料庫結構有變,我請他生成一個清除資料庫的腳本,讓他能正常使用新的結構的資料。
不過出現了一些問題,剛剛在改UUID的時候一些import與變數名稱沒有改好,有一些遺漏,我請AI同事再度去修正這些問題。
這也算是AI同事的老毛病了,如果是比較大的動作,通常無法一次到位。
AI同事改完後給他出錯的地方,他會再去修改直到錯誤被修正。
在測試完刪除資料庫,並且重新新增管理者帳號後,這次的修改就完成,我請AI同事將這次修改內容更新到檔案並做版本控制。