---
title: A1 檢查結果：AI 聊天機器人
---

# A1 檢查結果：AI 聊天機器人

> 檢查日期 2026-09-10｜耗時 65 分鐘｜對應卡片 CM-1637

---

## 🔴 一句話結論

**聊天功能沒有任何長度或次數限制——任何一個普通員工帳號，開四到八個連線就能讓整個系統停止回應，或者無限次呼叫把公司的 AI 額度燒光。**

---

## 這一棒在檢查什麼

系統裡有一個 AI 聊天機器人：使用者打字問問題，系統把訊息**原封不動送給 Anthropic（Claude）**，拿回覆顯示給使用者。對話記錄存在 Redis，一小時沒動就自動清掉。

這一棒檢查這個套件的 13 個檔（632 行）——**能不能看到別人的對話、能不能塞東西進別人的記錄、能不能無限次呼叫把公司的錢燒光**。

**這支跟前面掃過的都不一樣**：它是 25 支套件裡唯一會把使用者打的字送到公司網路外面、而且每次呼叫直接產生金錢成本的。

---

## 找到什麼

| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 修正卡 |
|---|---|---|---|---|---|---|
| 1 | 🟡 **中** | **聊天訊息沒有長度上限，呼叫次數也沒有限制** | **兩種後果，第一種更嚴重**：<br>① **整個產品停止回應**——系統只有 4 個工作程序，一個一次處理一個請求。攻擊者開 4～8 個連線各送一則接近 50MB 的訊息，就佔滿全部工作程序，**其他人連登入都會逾時**<br>② **燒公司的錢**——AI 金鑰是全公司共用一把，任何登入者可無限次呼叫 | ① 一個普通員工帳號（端點有要求登入，但**沒有任何角色檢查**）<br>② 系統用預設方式部署 | `api/ai_bot_route.py:64` | **待開** |

**只找到這 1 條。** 另外三條候選被檢查員否決（理由見下）。

---

## 詳細說明

### 問題 1：沒有長度上限，也沒有次數限制

**白話**：使用者在聊天框打什麼、打多長，系統完全不管，直接送給 Claude。也沒有「一個人一分鐘最多問幾次」的限制。

**為什麼「整個產品停止回應」比「燒錢」更嚴重**——檢查員把這條鏈追得很完整：

```
主專案跑 4 個同步工作程序（main.py:258）
    ↓ 一個工作程序一次只能處理一個請求
呼叫 Claude 本來就慢
    ↓ 而 Anthropic 套件的預設逾時是 10 分鐘，這個套件從來沒改過
工作程序被佔住
    ↓ 系統要等 120 秒才會強制砍掉卡住的工作程序
4～8 個並發請求 → 全部工作程序被佔滿
    ↓
整個產品的所有功能都停止回應，不只聊天
```

**單則訊息能多大**：主專案的全站請求上限是 50MB（CM-1587 訂的）。**這個上限擋得到聊天訊息，但 50MB 對一則聊天訊息來說等於沒擋。**

**在哪裡**：

```python
# 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 分鐘 |
