Guidant AI 資安檢視總報告 · 連線清單
系統把流程通知(任務指派、批次完成、專案啟動)推到客戶的 Discord 頻道或 Telegram 群組走的那條線。方向是單向往外送,鑰匙是一條 webhook 網址或一個 bot token——拿到就能以系統的名義往那個頻道發話。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這條線在六頁 STRIDE 裡零命中——不是沒問題,是通知模組的 M16-4(「測試 Discord」填什麼網址就打什麼)在 STRIDE 歸類時標的是 C02(入口在 API),沒標這條。本頁第四段改寫「依這條線的特性可能的攻擊手法」,M16-4 以引用方式列出。個別問題修了沒不在這頁講。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
ADM(["👤 租戶管理員<br/>(notify_config.update)"])
USR(["👤 任一登入使用者"])
subgraph MAIN["🏠 主系統 api"]
direction TB
CFG[("NOTIFY_CONFIG<br/>每租戶各一組<br/>DISCORD: webhook 網址<br/>TELEGRAM: bot token・chat_id")]
EVT["流程事件<br/>指派・批次完成・專案啟動"]
TH["背景執行緒<br/>(主執行緒先讀好設定再丟)"]
CFG --> TH
EVT --> TH
end
DC(["💬 Discord<br/>discord.com/api/webhooks/…"])
TG(["✈️ Telegram<br/>api.telegram.org/bot…"])
ADM -- "C02 · 填網址/token、測試" --> CFG
USR -- "C02 · 暱稱、專案名進訊息" --> EVT
TH == "C16 · HTTPS POST 純文字" ==> DC
TH == "C16 · HTTPS POST 純文字" ==> TG
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class ADM,USR,DC,TG ext
class CFG,EVT open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | guidant-api → Discord 官方 webhook 端點,或 Telegram Bot API。目的地:Discord 由設定頁填的 webhook 網址決定(只放行 discord.com 系六個官方網域、https、路徑 /api/webhooks/);Telegram 固定 https://api.telegram.org/bot{token}/sendMessage |
| 協定/埠 | HTTPS,requests.post();憑證驗證是 requests 預設開。兩支都沒帶 timeout |
| 用誰的身分 | Discord:webhook 網址本身就是憑證(誰拿到誰能發);Telegram:bot token(放在網址路徑裡)+ chat_id。兩者都是客戶在自己的 Discord/Telegram 上建的 |
| 設定存哪 | NOTIFY_CONFIG 群組、每租戶各一列(DISCORD/TELEGRAM 兩個 key),值含 enabled、secret(網址或 token)、chat_id。讀取 API 會刪掉 secret 再回(套件 mask_secret_value(),NOTIFY_CONFIG 在遮罩群組);落庫明文 |
| 誰能改 | 能力點 notify_config.update(租戶管理員層級,不是總部);「測試」同一顆能力點,密碼欄沒改時從既存列補回 secret 再試 |
| 傳什麼 | 一段純文字(Discord content、Telegram text 帶 parse_mode: Markdown):事件描述+使用者暱稱+專案名+任務數。不跳脫(M06-27 修時刻意只跳脫信件版,聊天頻道吃純文字維持原樣) |
| 怎麼送 | 業務流程在主執行緒先讀好設定(get_channel_config 要求有 user_context,背景執行緒沒有 RLS 身分會直接拒讀),再開 threading.Thread 丟出去,不等結果 |
| 何時存在 | 依設定(enabled 且 secret 齊全;Telegram 另要 chat_id)。沒設時記一行 warning 略過 |
依據:套件 jedi_notification/infra/discord/discord_adapter.py(is_allowed_webhook_url()、_ALLOWED_HOSTS)、jedi_notification/infra/telegram/telegram_adapter.py;主專案 app/notification/service/notification_service.py(get_channel_config、兩支 send)、app/notify_config/service/notify_config_test_service.py、api/notify_config/routes/notify_config_route.py(@require_capability("notify_config.update"))、呼叫點 app/flow_control/service/project_service.py:776-782、job_batch_complete_service.py:194-200、app/flow_engine/service/workflow_execution_service.py:1225-1297;套件 jedi_system_core/plugin/contract.py(DEFAULT_SECRET_MASKED_GROUPS 含 NOTIFY_CONFIG)。
DFD 對照:DFD 寫「webhook URL/bot token(NOTIFY_CONFIG)」與實況相符。補充 DFD 沒寫的:Discord 網址有官方網域白名單(CM-2371 後)、Telegram 固定官方、守門是租戶層級能力點(每家各自設,不是全系統一份)。
| 群 | 功能 | 對威脅的意義 |
|---|---|---|
| ① 流程通知 | 批次指派任務(notify_users_batch_assigned)、批次完成彙總(job_batch_complete_service)、專案啟動(_notify_project_started)——與 C10 信件版同三個事件 |
訊息裡有使用者自填的暱稱與專案名,原樣送出不跳脫;Telegram 端開了 Markdown 解析 |
| ② 測試 | 通知設定頁「測試」按鈕(POST /notify-config/{channel}/test)→ 用畫面上的設定(密碼欄沒改就補回存著的)送一則測試訊息 |
目的地由操作者當場指定——Discord 有白名單擋、Telegram 固定官方 |
沒有第三群:這條線不收回應、不讀頻道內容、不做指令互動。
這條線在 STRIDE 六頁零命中。依它的特性——「網址即憑證、目的地由人填、訊息夾著別人的字、不等結果」——列出三種可能的手法。前兩種是從同模組的 M16-4(STRIDE 標 C02)與 C10 T4 延伸來的,第三種是純粹依特性推。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | —(無掃描實例) | 拿「測試」按鈕當跳板打內網 | ① 有通知設定權限的租戶管理員打開設定頁;② 把 Discord webhook 網址填成內網位址(例如 http://10.0.0.5:8080/admin);③ 按「測試」;④ 系統替他對那個位址送一個 POST;⑤ 回應內容不會回給他,但「連不連得上」會——可以逐台探測客戶內網哪台活著 |
拿系統當跳板探測內網拓撲(偷不到資料,只能探) | E 權限提升、I 資料外洩 | (STRIDE 歸 C02)M16-4「測試 Discord 群組」填什麼網址就打什麼,見 M16 模組頁問題一覽第 4 條 ⚪ |
| T2 | —(無掃描實例) | 用系統的名義在客戶頻道發釣魚訊息 | ① 任一登入帳號把暱稱改成一段含連結的文字,Telegram 端可用 Markdown 寫成 [請重新登入](https://假網址);② 他去批次完成任務或啟動專案;③ 系統以 bot/webhook 身分把訊息推到客戶的頻道,顯示為系統發的;④ 頻道裡的同事點了就上當 |
用系統的頻道身分發釣魚訊息;Telegram Markdown 可偽裝連結文字 | S 冒充身分 | 同型:M06-27 通知信把暱稱原樣塞進信件 ⚪(修時刻意只跳脫信件版、聊天頻道維持原樣) |
| T3 | —(無掃描實例) | webhook 網址外流後冒充系統發話 | ① 攻擊者拿到 webhook 網址或 bot token(DB 備份、診斷包、設定頁截圖、Discord 端的分享);② 直接對 Discord 官方 webhook POST,不經過我們系統;③ 在客戶頻道以「稽核系統」的名義發任何訊息——假的任務指派、假的「請到此連結完成稽核」 | 冒充系統身分對客戶發話;客戶無從分辨 | S 冒充身分 | — |
三種手法的共同點:這條線的憑證是一條網址——拿到就能用、不需要我們系統的任何身分(T3);而我們拿它發出去的內容又夾著別人的字(T2)。T1 是「目的地由人填」的老病,Discord 那半 CM-2371 已用白名單擋住,但同一個「測試」端點的 Telegram 半邊目的地固定官方、天然不受影響。
對應三種手法與這條線的本質,防線分五條。沒有掃描實例,所以全部標 ◇——這些是依連線特性應有的標準防線。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 ◇ | 目的地白名單:只准官方網域、https、固定路徑前綴;host 精確比對不用 endswith;帶帳密或非預設埠一律拒 | T1 | ✅ Discord:is_allowed_webhook_url() 六個官方 host 精確比對、https、埠 443、路徑 /api/webhooks/,在套件層擋(測試與正式發送都過這支);不合格記 warning 只帶 host 不帶完整網址(網址即憑證)。✅ Telegram:網址寫死官方,token 只進路徑。驗證:CM-2371 |
| D2 ◇ | 使用者的字進訊息前做對應格式的跳脫。Telegram 開了 Markdown 就要跳脫 Markdown 特殊字元;Discord 純文字至少去掉 @everyone/@here 與連結語法 |
T2 | ⚠️ 沒有。M06-27 修時決定「只跳脫信件版、聊天頻道維持原樣」;Telegram parse_mode: Markdown 仍開,暱稱裡的 [文字](網址) 會被渲染成可點連結。這是當時的取捨,記在這裡是因為它直接決定 T2 成不成立 |
| D3 ◇ | 憑證加密落庫、讀取遮罩、日誌不記網址 | T3 的外流面 | ✅ 讀取遮罩(mask_secret_value() 刪 secret);adapter 的 log 只記 host 不記完整網址。⚠️ 落庫明文(與 SMTP、LDAP、issue token 同一組問題,見 C10 D5) |
| D4 ◇ | 外部呼叫有逾時、失敗有紀錄。背景執行緒丟出去不等結果是對的,但沒逾時的 requests.post 會讓執行緒永遠掛著 |
外部慢時執行緒累積 | ⚠️ 兩支 adapter 的 requests.post() 都沒帶 timeout(requests 預設無限等);每次事件開一條 thread,Discord 掛了執行緒會堆積。失敗有記 error |
| D5 ◇ | 訊息內容最小化:頻道是第三方 SaaS,推出去的只該有「有事發生、去系統看」,不帶客戶內部的專案名與人名 | 客戶資料經這條線流到 Discord/Telegram 伺服器 | ⚠️ 目前訊息帶專案名、暱稱、任務數。對合規客戶(不准資料出境)這是一條沒標示的出境路徑;設定頁沒有說明 |
最便宜的一步:D4——兩支 requests.post() 各加 timeout=10,一行一個。D2 的 Telegram Markdown 關掉(或改 MarkdownV2 並跳脫)是第二便宜的。
[連結](url) 的帳號實測一次批次完成通知,看 Telegram 端渲染出什麼。@everyone 濫用(D2):Discord webhook 預設允許 @everyone——暱稱含它會讓整個伺服器被 ping。依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本,本條零命中)、docs/security-report/M16-notification.md 第 4 條(M16-4,STRIDE 標 C02)、套件 jedi_notification/infra/{discord,telegram}/*_adapter.py、主專案 app/notification/service/notification_service.py、app/notify_config/service/notify_config_test_service.py、api/notify_config/routes/notify_config_route.py、三個流程事件呼叫點、套件 jedi_system_core/plugin/contract.py、DFD Level 0。