檢查日期 2026-09-10|耗時 65 分鐘|對應卡片 CM-1637
聊天功能沒有任何長度或次數限制——任何一個普通員工帳號,開四到八個連線就能讓整個系統停止回應,或者無限次呼叫把公司的 AI 額度燒光。
系統裡有一個 AI 聊天機器人:使用者打字問問題,系統把訊息原封不動送給 Anthropic(Claude),拿回覆顯示給使用者。對話記錄存在 Redis,一小時沒動就自動清掉。
這一棒檢查這個套件的 13 個檔(632 行)——能不能看到別人的對話、能不能塞東西進別人的記錄、能不能無限次呼叫把公司的錢燒光。
這支跟前面掃過的都不一樣:它是 25 支套件裡唯一會把使用者打的字送到公司網路外面、而且每次呼叫直接產生金錢成本的。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 修正卡 |
|---|---|---|---|---|---|---|
| 1 | 🟡 中 | 聊天訊息沒有長度上限,呼叫次數也沒有限制 | 兩種後果,第一種更嚴重: ① 整個產品停止回應——系統只有 4 個工作程序,一個一次處理一個請求。攻擊者開 4~8 個連線各送一則接近 50MB 的訊息,就佔滿全部工作程序,其他人連登入都會逾時 ② 燒公司的錢——AI 金鑰是全公司共用一把,任何登入者可無限次呼叫 |
① 一個普通員工帳號(端點有要求登入,但沒有任何角色檢查) ② 系統用預設方式部署 |
api/ai_bot_route.py:64 |
待開 |
只找到這 1 條。 另外三條候選被檢查員否決(理由見下)。
白話:使用者在聊天框打什麼、打多長,系統完全不管,直接送給 Claude。也沒有「一個人一分鐘最多問幾次」的限制。
為什麼「整個產品停止回應」比「燒錢」更嚴重——檢查員把這條鏈追得很完整:
主專案跑 4 個同步工作程序(main.py:258)
↓ 一個工作程序一次只能處理一個請求
呼叫 Claude 本來就慢
↓ 而 Anthropic 套件的預設逾時是 10 分鐘,這個套件從來沒改過
工作程序被佔住
↓ 系統要等 120 秒才會強制砍掉卡住的工作程序
4~8 個並發請求 → 全部工作程序被佔滿
↓
整個產品的所有功能都停止回應,不只聊天
單則訊息能多大:主專案的全站請求上限是 50MB(CM-1587 訂的)。這個上限擋得到聊天訊息,但 50MB 對一則聊天訊息來說等於沒擋。
在哪裡:
# jedi_ai_bot/api/ai_bot_route.py
第 57 行 message = (payload.get('message') or '').strip() ← 只去頭尾空白
第 60 行 if not message: return ...400 ← 只檢查空字串
第 64 行 reply = ctx.service.chat(ctx.current_user_id(), session_id, message)
↑ 沒有長度檢查、沒有次數限制,直接送出去檢查員做了額外查證:他們去主專案和 nginx 設定裡找有沒有其他層擋著——沒有找到任何頻率限制。
怎麼修(工具給的建議很具體,首腦認同):
| 做什麼 | 放哪裡 |
|---|---|
訊息長度上限(建議 4000 字)、session_id 長度上限(建議 64 字),超過回 400 |
套件的 route 層(post),上限做成 AiBotConfig 可調欄位 |
| 呼叫 Claude 的逾時 | AiBotService.__init__ 加 request_timeout 參數傳給 Anthropic(...),讓上游卡住時自己放棄,不要等系統砍工作程序 |
| 每人呼叫頻率限制 | 做成 adapter 讓主專案用既有的 Redis 實作——「怎麼限制」是產品知識,但「有沒有限制」這個插槽該由套件開出來 |
我原本的擔心:對話記錄的 Redis 儲存位置是 chat_history:{使用者編號}:{對話編號},而對話編號完全由呼叫端自己給,只做了去空白處理(ai_bot_route.py:58)。Redis 的儲存位置沒有跳脫機制,冒號在這裡是有意義的分隔符——所以我擔心有人塞 x:chat_history:999 之類的東西跨到別人的記錄。
實際查證結果:跨不過去。
儲存位置 = chat_history:{使用者編號}:{對話編號}
↑ 這一段由伺服器填(api/ai/__init__.py:34 的 get_user_context().id)
攻擊者(編號 5)能產生的位置一律是 chat_history:5:<任意內容>
想碰的目標(編號 9)的位置是 chat_history:9:default
第一段是伺服器填的、而且是數字不含冒號 → 前綴偽造不了
我原本的擔心是錯的。
但這條仍值得記:這個保護完全依賴「使用者編號由伺服器填、且排在最前面」。哪天有人把編號改成呼叫端可控、或把順序調成對話編號在前,這道保護就沒了。建議在程式碼加一行註解說明為什麼順序不能改。
背景:這支套件把認證設定做成「必填欄位」,但沒有任何明確的檢查程式碼(首腦已確認整支 plugin.py 沒有 raise,也沒有建構後驗證)。這跟 jedi-issue 那支不同——那支會直接報錯拒絕掛載。
實測結果:
| 情況 | 結果 |
|---|---|
AiBotAdapters(auth_required=None) 建構 |
✅ 成功(必填欄位擋不住 None) |
但接著 plugin.py:223 呼叫 adapters.auth_required(...) |
❌ TypeError 炸掉 |
所以結果是安全的——系統啟動時就會炸,不會變成「不用登入就能用」的端點。
不如 jedi-issue 那支友善(那支會給明確的中文錯誤訊息說「拒絕掛載,因為會讓端點變公開」),但安全性上沒有洞。這一項不開卡。
| 候選 | 否決票數 | 理由 |
|---|---|---|
| Redis 對話記錄無限增長 | 2:1 否決 | 記錄是在 Claude 接受請求之後才寫入的,所以 AI 模型自己的內容長度上限就是天花板,遠低於 HTTP 的 50MB;而且每筆記錄都有 1 小時自動清除 |
開發用啟動檔寫死 debug=True |
3:0 否決 | Flask 沒指定主機時只綁本機、除錯器需要 PIN 且只信任本機、而且開發用檔案不會被打包進發行版 |
這是六個檢查案以來 stamp 最乾淨的一次:verified,而且連「拒收原因」欄位都是空的(前一個案子六棒有五棒帶著 findings-refused 標記)。
三個獨立檢查員對 3 條候選各投一票,9 票全數投出、沒有漏投、沒有中斷。F1 是 3 票全過。
檢查員還做了額外查證:他們去主專案和 nginx 設定裡找有沒有頻率限制,確認沒有——這不是讀套件就能知道的,是主動追出去查的。
快篩模式。13 個檔裡有 4 個是空的,實際有內容的 9 個。工具也沒有留下逐檔閱讀紀錄。
而且首腦補查證明了工具沒照卡片的重點清單走——我點名要查的兩項(對話記錄能不能跨人、缺認證設定會怎樣)工具一項都沒答,是首腦自己查的。兩項結論都是好消息,但那不是工具給的。
卡片寫「檔數 13」,工具報告寫 14。查了原因:工具算全部受版控檔,我給的驗證指令只算 .py 檔,差的是 harness/docker-compose.yml。
掃描範圍沒問題,反而多掃一個檔(那個檔也確實該掃,它可能有寫死的憑證)。但驗證指令的口徑該跟工具一致,記錄下來給下一棒參考。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 14 個受版控檔(13 支 .py + 1 支 docker-compose) |
| 派出/回報的研究員 | 2 / 2 |
| 候選問題 → 去除重複 | 4 → 3 |
| 投票數 | 9(3 條 × 3 個檢查員) |
| 沒投到票的 | 0 |
| 投票中斷的 | 0 |
| 被檢查員否決的 | 2 |
| stamp 狀態 | verified(無拒收原因,最乾淨的一次) |
| 耗時 | 65 分鐘 |